sigil_parser/plurality/
lexer.rs

1//! # Plurality Lexer Extensions
2//!
3//! Extends the Sigil lexer with tokens for plurality constructs:
4//! - Keywords: `alter`, `switch`, `headspace`, `cocon`, `reality`, `split`
5//! - Alter-source markers: `@!`, `@~`, `@?`, `@‽`
6//! - Forced operators: `switch!`, `split!`
7//!
8//! ## Integration with Main Lexer
9//!
10//! These tokens should be added to `lexer.rs` Token enum:
11//!
12//! ```rust,ignore
13//! // Plurality keywords
14//! #[token("alter")]
15//! Alter,
16//! #[token("switch")]
17//! Switch,
18//! #[token("headspace")]
19//! Headspace,
20//! #[token("cocon")]
21//! CoCon,
22//! #[token("reality")]
23//! Reality,
24//! #[token("split")]
25//! Split,
26//! #[token("trigger")]
27//! Trigger,
28//! #[token("location")]
29//! Location,
30//! #[token("layer")]
31//! Layer,
32//! #[token("transform")]
33//! Transform,
34//! #[token("states")]
35//! States,
36//! #[token("anima")]
37//! Anima,
38//!
39//! // Alter-source markers (compound tokens)
40//! #[token("@!")]
41//! AlterSourceFronting,    // Authoritative from fronting alter
42//! #[token("@~")]
43//! AlterSourceCoCon,       // Reported from co-conscious
44//! #[token("@?")]
45//! AlterSourceDormant,     // Uncertain from dormant
46//! #[token("@‽")]
47//! AlterSourceBlended,     // Paradoxical from blended state
48//!
49//! // Forced operation variants
50//! #[token("switch!")]
51//! SwitchForced,           // Forced switch (bypasses deliberation)
52//! #[token("split!")]
53//! SplitForced,            // Forced split (trauma response)
54//! ```
55
56use crate::lexer::Token;
57use crate::span::Span;
58
59// ============================================================================
60// PLURALITY TOKEN CATEGORIES
61// ============================================================================
62
63/// Check if a token is a plurality keyword
64pub fn is_plurality_keyword(token: &Token) -> bool {
65    matches!(
66        token,
67        Token::Ident(s) if matches!(s.as_str(),
68            "alter" | "switch" | "headspace" | "cocon" |
69            "reality" | "split" | "trigger" | "location" |
70            "layer" | "transform" | "states" | "anima"
71        )
72    )
73}
74
75/// Check if a token is an alter category keyword
76pub fn is_alter_category(token: &Token) -> bool {
77    matches!(
78        token,
79        Token::Ident(s) if matches!(s.as_str(),
80            "Council" | "Servant" | "Fragment" | "Hidden" | "Persecutor"
81        )
82    )
83}
84
85/// Check if a token is an alter state keyword
86pub fn is_alter_state(token: &Token) -> bool {
87    matches!(
88        token,
89        Token::Ident(s) if matches!(s.as_str(),
90            "Dormant" | "Stirring" | "CoConscious" | "Emerging" |
91            "Fronting" | "Receding" | "Triggered" | "Dissociating"
92        )
93    )
94}
95
96/// Check if a token sequence represents an alter-source marker
97/// Returns (is_alter_source, consumed_count)
98pub fn check_alter_source_sequence(tokens: &[(Token, Span)]) -> Option<AlterSourceMarker> {
99    if tokens.is_empty() {
100        return None;
101    }
102
103    // Check for @ followed by evidentiality marker
104    if let Token::At = &tokens[0].0 {
105        if tokens.len() >= 2 {
106            match &tokens[1].0 {
107                Token::Bang => Some(AlterSourceMarker::Fronting),
108                Token::Tilde => Some(AlterSourceMarker::CoCon),
109                Token::Question => Some(AlterSourceMarker::Dormant),
110                Token::Interrobang => Some(AlterSourceMarker::Blended),
111                Token::Ident(name) if name == "Fronting" => Some(AlterSourceMarker::Fronting),
112                Token::Ident(name) if name == "CoCon" => Some(AlterSourceMarker::CoCon),
113                Token::Ident(name) if name == "Dormant" => Some(AlterSourceMarker::Dormant),
114                Token::Ident(name) if name == "Blended" => Some(AlterSourceMarker::Blended),
115                _ => None,
116            }
117        } else {
118            None
119        }
120    } else {
121        None
122    }
123}
124
125/// Check if a token sequence represents a forced operation
126/// (e.g., `switch!` or `split!`)
127pub fn check_forced_operation(tokens: &[(Token, Span)]) -> Option<ForcedOperation> {
128    if tokens.len() < 2 {
129        return None;
130    }
131
132    if let Token::Ident(name) = &tokens[0].0 {
133        if let Token::Bang = &tokens[1].0 {
134            match name.as_str() {
135                "switch" => return Some(ForcedOperation::Switch),
136                "split" => return Some(ForcedOperation::Split),
137                _ => {}
138            }
139        }
140    }
141
142    None
143}
144
145// ============================================================================
146// PLURALITY TOKEN TYPES
147// ============================================================================
148
149/// Alter-source marker type
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum AlterSourceMarker {
152    /// `@!` or `@Fronting` - authoritative from fronting alter
153    Fronting,
154    /// `@~` or `@CoCon` - reported from co-conscious alter
155    CoCon,
156    /// `@?` or `@Dormant` - uncertain from dormant alter
157    Dormant,
158    /// `@‽` or `@Blended` - paradoxical from blended state
159    Blended,
160}
161
162impl AlterSourceMarker {
163    /// Convert to evidentiality marker equivalent
164    pub fn to_evidentiality(&self) -> &'static str {
165        match self {
166            AlterSourceMarker::Fronting => "!",
167            AlterSourceMarker::CoCon => "~",
168            AlterSourceMarker::Dormant => "?",
169            AlterSourceMarker::Blended => "‽",
170        }
171    }
172
173    /// Get the symbol representation
174    pub fn symbol(&self) -> &'static str {
175        match self {
176            AlterSourceMarker::Fronting => "@!",
177            AlterSourceMarker::CoCon => "@~",
178            AlterSourceMarker::Dormant => "@?",
179            AlterSourceMarker::Blended => "@‽",
180        }
181    }
182}
183
184/// Forced operation type
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum ForcedOperation {
187    /// `switch!` - forced switch bypassing deliberation
188    Switch,
189    /// `split!` - forced split (trauma response)
190    Split,
191}
192
193// ============================================================================
194// PLURALITY TOKEN STREAM HELPERS
195// ============================================================================
196
197/// Iterator adapter for plurality token processing
198pub struct PluralityTokenStream<'a> {
199    tokens: &'a [(Token, Span)],
200    position: usize,
201}
202
203impl<'a> PluralityTokenStream<'a> {
204    pub fn new(tokens: &'a [(Token, Span)]) -> Self {
205        Self {
206            tokens,
207            position: 0,
208        }
209    }
210
211    /// Peek at the current token
212    pub fn peek(&self) -> Option<&(Token, Span)> {
213        self.tokens.get(self.position)
214    }
215
216    /// Peek at the next n tokens
217    pub fn peek_n(&self, n: usize) -> &[(Token, Span)] {
218        let end = (self.position + n).min(self.tokens.len());
219        &self.tokens[self.position..end]
220    }
221
222    /// Advance by n tokens
223    pub fn advance(&mut self, n: usize) {
224        self.position = (self.position + n).min(self.tokens.len());
225    }
226
227    /// Check if we're at a plurality keyword
228    pub fn at_plurality_keyword(&self) -> bool {
229        self.peek()
230            .map(|(t, _)| is_plurality_keyword(t))
231            .unwrap_or(false)
232    }
233
234    /// Try to consume an alter-source marker
235    pub fn try_alter_source(&mut self) -> Option<(AlterSourceMarker, Span)> {
236        let lookahead = self.peek_n(2);
237        if let Some(marker) = check_alter_source_sequence(lookahead) {
238            let start = lookahead[0].1.start;
239            let end = lookahead[1].1.end;
240            self.advance(2);
241            Some((marker, Span::new(start, end)))
242        } else {
243            None
244        }
245    }
246
247    /// Try to consume a forced operation
248    pub fn try_forced_operation(&mut self) -> Option<(ForcedOperation, Span)> {
249        let lookahead = self.peek_n(2);
250        if let Some(op) = check_forced_operation(lookahead) {
251            let start = lookahead[0].1.start;
252            let end = lookahead[1].1.end;
253            self.advance(2);
254            Some((op, Span::new(start, end)))
255        } else {
256            None
257        }
258    }
259
260    /// Check if we're at an alter definition start
261    /// (`alter Ident: Category` or `alter Ident {`)
262    pub fn at_alter_def(&self) -> bool {
263        let lookahead = self.peek_n(4);
264        if lookahead.is_empty() {
265            return false;
266        }
267
268        // Check for `alter` keyword
269        matches!(&lookahead[0].0, Token::Ident(s) if s == "alter")
270            && lookahead.len() > 1
271            && matches!(&lookahead[1].0, Token::Ident(_))
272    }
273
274    /// Check if we're at a switch expression
275    /// (`switch to Alter` or `switch! to Alter`)
276    pub fn at_switch_expr(&self) -> bool {
277        let lookahead = self.peek_n(3);
278        if lookahead.is_empty() {
279            return false;
280        }
281
282        match &lookahead[0].0 {
283            Token::Ident(s) if s == "switch" => {
284                // Check for `switch to` or `switch! to`
285                if lookahead.len() > 1 {
286                    match &lookahead[1].0 {
287                        Token::Bang => {
288                            // switch! to ...
289                            lookahead.len() > 2
290                                && matches!(&lookahead[2].0, Token::Ident(s) if s == "to")
291                        }
292                        Token::Ident(s) if s == "to" => true,
293                        _ => false,
294                    }
295                } else {
296                    false
297                }
298            }
299            _ => false,
300        }
301    }
302
303    /// Check if we're at a headspace definition
304    pub fn at_headspace_def(&self) -> bool {
305        let lookahead = self.peek_n(2);
306        !lookahead.is_empty()
307            && matches!(&lookahead[0].0, Token::Ident(s) if s == "headspace")
308            && lookahead.len() > 1
309            && matches!(&lookahead[1].0, Token::Ident(_))
310    }
311
312    /// Check if we're at a reality definition
313    pub fn at_reality_def(&self) -> bool {
314        let lookahead = self.peek_n(3);
315        if lookahead.len() < 3 {
316            return false;
317        }
318
319        matches!(&lookahead[0].0, Token::Ident(s) if s == "reality")
320            && matches!(&lookahead[1].0, Token::Ident(s) if s == "entity")
321            && matches!(&lookahead[2].0, Token::Ident(_))
322    }
323
324    /// Check if we're at a co-con channel definition
325    /// (`cocon<A, B> name { ... }`)
326    pub fn at_cocon_channel(&self) -> bool {
327        let lookahead = self.peek_n(2);
328        !lookahead.is_empty()
329            && matches!(&lookahead[0].0, Token::Ident(s) if s == "cocon")
330            && lookahead.len() > 1
331            && matches!(&lookahead[1].0, Token::Lt)
332    }
333
334    /// Check if we're at a trigger handler
335    /// (`on trigger Name { ... }`)
336    pub fn at_trigger_handler(&self) -> bool {
337        let lookahead = self.peek_n(3);
338        if lookahead.len() < 3 {
339            return false;
340        }
341
342        matches!(&lookahead[0].0, Token::On)
343            && matches!(&lookahead[1].0, Token::Ident(s) if s == "trigger")
344            && matches!(&lookahead[2].0, Token::Ident(_))
345    }
346
347    /// Check if we're at a split expression
348    /// (`split! from Alter { ... }`)
349    pub fn at_split_expr(&self) -> bool {
350        let lookahead = self.peek_n(3);
351        if lookahead.is_empty() {
352            return false;
353        }
354
355        matches!(&lookahead[0].0, Token::Ident(s) if s == "split")
356            && lookahead.len() > 1
357            && matches!(&lookahead[1].0, Token::Bang)
358    }
359}
360
361// ============================================================================
362// TESTS
363// ============================================================================
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    fn make_token(token: Token) -> (Token, Span) {
370        (token, Span::default())
371    }
372
373    #[test]
374    fn test_alter_source_markers() {
375        // Test @! (fronting)
376        let tokens = vec![make_token(Token::At), make_token(Token::Bang)];
377        assert_eq!(
378            check_alter_source_sequence(&tokens),
379            Some(AlterSourceMarker::Fronting)
380        );
381
382        // Test @~ (co-con)
383        let tokens = vec![make_token(Token::At), make_token(Token::Tilde)];
384        assert_eq!(
385            check_alter_source_sequence(&tokens),
386            Some(AlterSourceMarker::CoCon)
387        );
388
389        // Test @? (dormant)
390        let tokens = vec![make_token(Token::At), make_token(Token::Question)];
391        assert_eq!(
392            check_alter_source_sequence(&tokens),
393            Some(AlterSourceMarker::Dormant)
394        );
395
396        // Test @‽ (blended)
397        let tokens = vec![make_token(Token::At), make_token(Token::Interrobang)];
398        assert_eq!(
399            check_alter_source_sequence(&tokens),
400            Some(AlterSourceMarker::Blended)
401        );
402    }
403
404    #[test]
405    fn test_forced_operations() {
406        // Test switch!
407        let tokens = vec![
408            make_token(Token::Ident("switch".to_string())),
409            make_token(Token::Bang),
410        ];
411        assert_eq!(
412            check_forced_operation(&tokens),
413            Some(ForcedOperation::Switch)
414        );
415
416        // Test split!
417        let tokens = vec![
418            make_token(Token::Ident("split".to_string())),
419            make_token(Token::Bang),
420        ];
421        assert_eq!(
422            check_forced_operation(&tokens),
423            Some(ForcedOperation::Split)
424        );
425    }
426
427    #[test]
428    fn test_plurality_keywords() {
429        assert!(is_plurality_keyword(&Token::Ident("alter".to_string())));
430        assert!(is_plurality_keyword(&Token::Ident("switch".to_string())));
431        assert!(is_plurality_keyword(&Token::Ident("headspace".to_string())));
432        assert!(is_plurality_keyword(&Token::Ident("cocon".to_string())));
433        assert!(!is_plurality_keyword(&Token::Ident("struct".to_string())));
434    }
435
436    #[test]
437    fn test_alter_categories() {
438        assert!(is_alter_category(&Token::Ident("Council".to_string())));
439        assert!(is_alter_category(&Token::Ident("Servant".to_string())));
440        assert!(is_alter_category(&Token::Ident("Fragment".to_string())));
441        assert!(!is_alter_category(&Token::Ident("Other".to_string())));
442    }
443
444    #[test]
445    fn test_alter_states() {
446        assert!(is_alter_state(&Token::Ident("Dormant".to_string())));
447        assert!(is_alter_state(&Token::Ident("Fronting".to_string())));
448        assert!(is_alter_state(&Token::Ident("CoConscious".to_string())));
449        assert!(!is_alter_state(&Token::Ident("Running".to_string())));
450    }
451
452    #[test]
453    fn test_token_stream_alter_def() {
454        let tokens = vec![
455            make_token(Token::Ident("alter".to_string())),
456            make_token(Token::Ident("Abaddon".to_string())),
457            make_token(Token::Colon),
458            make_token(Token::Ident("Council".to_string())),
459        ];
460        let stream = PluralityTokenStream::new(&tokens);
461        assert!(stream.at_alter_def());
462    }
463
464    #[test]
465    fn test_token_stream_switch_expr() {
466        // Regular switch
467        let tokens = vec![
468            make_token(Token::Ident("switch".to_string())),
469            make_token(Token::Ident("to".to_string())),
470            make_token(Token::Ident("Beleth".to_string())),
471        ];
472        let stream = PluralityTokenStream::new(&tokens);
473        assert!(stream.at_switch_expr());
474
475        // Forced switch
476        let tokens = vec![
477            make_token(Token::Ident("switch".to_string())),
478            make_token(Token::Bang),
479            make_token(Token::Ident("to".to_string())),
480            make_token(Token::Ident("Abaddon".to_string())),
481        ];
482        let stream = PluralityTokenStream::new(&tokens);
483        assert!(stream.at_switch_expr());
484    }
485
486    #[test]
487    fn test_token_stream_headspace() {
488        let tokens = vec![
489            make_token(Token::Ident("headspace".to_string())),
490            make_token(Token::Ident("InnerWorld".to_string())),
491            make_token(Token::LBrace),
492        ];
493        let stream = PluralityTokenStream::new(&tokens);
494        assert!(stream.at_headspace_def());
495    }
496
497    #[test]
498    fn test_token_stream_cocon() {
499        let tokens = vec![
500            make_token(Token::Ident("cocon".to_string())),
501            make_token(Token::Lt),
502            make_token(Token::Ident("Stolas".to_string())),
503            make_token(Token::Comma),
504            make_token(Token::Ident("Paimon".to_string())),
505            make_token(Token::Gt),
506        ];
507        let stream = PluralityTokenStream::new(&tokens);
508        assert!(stream.at_cocon_channel());
509    }
510}