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 { tokens, position: 0 }
206    }
207
208    /// Peek at the current token
209    pub fn peek(&self) -> Option<&(Token, Span)> {
210        self.tokens.get(self.position)
211    }
212
213    /// Peek at the next n tokens
214    pub fn peek_n(&self, n: usize) -> &[(Token, Span)] {
215        let end = (self.position + n).min(self.tokens.len());
216        &self.tokens[self.position..end]
217    }
218
219    /// Advance by n tokens
220    pub fn advance(&mut self, n: usize) {
221        self.position = (self.position + n).min(self.tokens.len());
222    }
223
224    /// Check if we're at a plurality keyword
225    pub fn at_plurality_keyword(&self) -> bool {
226        self.peek().map(|(t, _)| is_plurality_keyword(t)).unwrap_or(false)
227    }
228
229    /// Try to consume an alter-source marker
230    pub fn try_alter_source(&mut self) -> Option<(AlterSourceMarker, Span)> {
231        let lookahead = self.peek_n(2);
232        if let Some(marker) = check_alter_source_sequence(lookahead) {
233            let start = lookahead[0].1.start;
234            let end = lookahead[1].1.end;
235            self.advance(2);
236            Some((marker, Span::new(start, end)))
237        } else {
238            None
239        }
240    }
241
242    /// Try to consume a forced operation
243    pub fn try_forced_operation(&mut self) -> Option<(ForcedOperation, Span)> {
244        let lookahead = self.peek_n(2);
245        if let Some(op) = check_forced_operation(lookahead) {
246            let start = lookahead[0].1.start;
247            let end = lookahead[1].1.end;
248            self.advance(2);
249            Some((op, Span::new(start, end)))
250        } else {
251            None
252        }
253    }
254
255    /// Check if we're at an alter definition start
256    /// (`alter Ident: Category` or `alter Ident {`)
257    pub fn at_alter_def(&self) -> bool {
258        let lookahead = self.peek_n(4);
259        if lookahead.is_empty() {
260            return false;
261        }
262
263        // Check for `alter` keyword
264        matches!(&lookahead[0].0, Token::Ident(s) if s == "alter")
265            && lookahead.len() > 1
266            && matches!(&lookahead[1].0, Token::Ident(_))
267    }
268
269    /// Check if we're at a switch expression
270    /// (`switch to Alter` or `switch! to Alter`)
271    pub fn at_switch_expr(&self) -> bool {
272        let lookahead = self.peek_n(3);
273        if lookahead.is_empty() {
274            return false;
275        }
276
277        match &lookahead[0].0 {
278            Token::Ident(s) if s == "switch" => {
279                // Check for `switch to` or `switch! to`
280                if lookahead.len() > 1 {
281                    match &lookahead[1].0 {
282                        Token::Bang => {
283                            // switch! to ...
284                            lookahead.len() > 2
285                                && matches!(&lookahead[2].0, Token::Ident(s) if s == "to")
286                        }
287                        Token::Ident(s) if s == "to" => true,
288                        _ => false,
289                    }
290                } else {
291                    false
292                }
293            }
294            _ => false,
295        }
296    }
297
298    /// Check if we're at a headspace definition
299    pub fn at_headspace_def(&self) -> bool {
300        let lookahead = self.peek_n(2);
301        !lookahead.is_empty()
302            && matches!(&lookahead[0].0, Token::Ident(s) if s == "headspace")
303            && lookahead.len() > 1
304            && matches!(&lookahead[1].0, Token::Ident(_))
305    }
306
307    /// Check if we're at a reality definition
308    pub fn at_reality_def(&self) -> bool {
309        let lookahead = self.peek_n(3);
310        if lookahead.len() < 3 {
311            return false;
312        }
313
314        matches!(&lookahead[0].0, Token::Ident(s) if s == "reality")
315            && matches!(&lookahead[1].0, Token::Ident(s) if s == "entity")
316            && matches!(&lookahead[2].0, Token::Ident(_))
317    }
318
319    /// Check if we're at a co-con channel definition
320    /// (`cocon<A, B> name { ... }`)
321    pub fn at_cocon_channel(&self) -> bool {
322        let lookahead = self.peek_n(2);
323        !lookahead.is_empty()
324            && matches!(&lookahead[0].0, Token::Ident(s) if s == "cocon")
325            && lookahead.len() > 1
326            && matches!(&lookahead[1].0, Token::Lt)
327    }
328
329    /// Check if we're at a trigger handler
330    /// (`on trigger Name { ... }`)
331    pub fn at_trigger_handler(&self) -> bool {
332        let lookahead = self.peek_n(3);
333        if lookahead.len() < 3 {
334            return false;
335        }
336
337        matches!(&lookahead[0].0, Token::On)
338            && matches!(&lookahead[1].0, Token::Ident(s) if s == "trigger")
339            && matches!(&lookahead[2].0, Token::Ident(_))
340    }
341
342    /// Check if we're at a split expression
343    /// (`split! from Alter { ... }`)
344    pub fn at_split_expr(&self) -> bool {
345        let lookahead = self.peek_n(3);
346        if lookahead.is_empty() {
347            return false;
348        }
349
350        matches!(&lookahead[0].0, Token::Ident(s) if s == "split")
351            && lookahead.len() > 1
352            && matches!(&lookahead[1].0, Token::Bang)
353    }
354}
355
356// ============================================================================
357// TESTS
358// ============================================================================
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    fn make_token(token: Token) -> (Token, Span) {
365        (token, Span::default())
366    }
367
368    #[test]
369    fn test_alter_source_markers() {
370        // Test @! (fronting)
371        let tokens = vec![
372            make_token(Token::At),
373            make_token(Token::Bang),
374        ];
375        assert_eq!(
376            check_alter_source_sequence(&tokens),
377            Some(AlterSourceMarker::Fronting)
378        );
379
380        // Test @~ (co-con)
381        let tokens = vec![
382            make_token(Token::At),
383            make_token(Token::Tilde),
384        ];
385        assert_eq!(
386            check_alter_source_sequence(&tokens),
387            Some(AlterSourceMarker::CoCon)
388        );
389
390        // Test @? (dormant)
391        let tokens = vec![
392            make_token(Token::At),
393            make_token(Token::Question),
394        ];
395        assert_eq!(
396            check_alter_source_sequence(&tokens),
397            Some(AlterSourceMarker::Dormant)
398        );
399
400        // Test @‽ (blended)
401        let tokens = vec![
402            make_token(Token::At),
403            make_token(Token::Interrobang),
404        ];
405        assert_eq!(
406            check_alter_source_sequence(&tokens),
407            Some(AlterSourceMarker::Blended)
408        );
409    }
410
411    #[test]
412    fn test_forced_operations() {
413        // Test switch!
414        let tokens = vec![
415            make_token(Token::Ident("switch".to_string())),
416            make_token(Token::Bang),
417        ];
418        assert_eq!(
419            check_forced_operation(&tokens),
420            Some(ForcedOperation::Switch)
421        );
422
423        // Test split!
424        let tokens = vec![
425            make_token(Token::Ident("split".to_string())),
426            make_token(Token::Bang),
427        ];
428        assert_eq!(
429            check_forced_operation(&tokens),
430            Some(ForcedOperation::Split)
431        );
432    }
433
434    #[test]
435    fn test_plurality_keywords() {
436        assert!(is_plurality_keyword(&Token::Ident("alter".to_string())));
437        assert!(is_plurality_keyword(&Token::Ident("switch".to_string())));
438        assert!(is_plurality_keyword(&Token::Ident("headspace".to_string())));
439        assert!(is_plurality_keyword(&Token::Ident("cocon".to_string())));
440        assert!(!is_plurality_keyword(&Token::Ident("struct".to_string())));
441    }
442
443    #[test]
444    fn test_alter_categories() {
445        assert!(is_alter_category(&Token::Ident("Council".to_string())));
446        assert!(is_alter_category(&Token::Ident("Servant".to_string())));
447        assert!(is_alter_category(&Token::Ident("Fragment".to_string())));
448        assert!(!is_alter_category(&Token::Ident("Other".to_string())));
449    }
450
451    #[test]
452    fn test_alter_states() {
453        assert!(is_alter_state(&Token::Ident("Dormant".to_string())));
454        assert!(is_alter_state(&Token::Ident("Fronting".to_string())));
455        assert!(is_alter_state(&Token::Ident("CoConscious".to_string())));
456        assert!(!is_alter_state(&Token::Ident("Running".to_string())));
457    }
458
459    #[test]
460    fn test_token_stream_alter_def() {
461        let tokens = vec![
462            make_token(Token::Ident("alter".to_string())),
463            make_token(Token::Ident("Abaddon".to_string())),
464            make_token(Token::Colon),
465            make_token(Token::Ident("Council".to_string())),
466        ];
467        let stream = PluralityTokenStream::new(&tokens);
468        assert!(stream.at_alter_def());
469    }
470
471    #[test]
472    fn test_token_stream_switch_expr() {
473        // Regular switch
474        let tokens = vec![
475            make_token(Token::Ident("switch".to_string())),
476            make_token(Token::Ident("to".to_string())),
477            make_token(Token::Ident("Beleth".to_string())),
478        ];
479        let stream = PluralityTokenStream::new(&tokens);
480        assert!(stream.at_switch_expr());
481
482        // Forced switch
483        let tokens = vec![
484            make_token(Token::Ident("switch".to_string())),
485            make_token(Token::Bang),
486            make_token(Token::Ident("to".to_string())),
487            make_token(Token::Ident("Abaddon".to_string())),
488        ];
489        let stream = PluralityTokenStream::new(&tokens);
490        assert!(stream.at_switch_expr());
491    }
492
493    #[test]
494    fn test_token_stream_headspace() {
495        let tokens = vec![
496            make_token(Token::Ident("headspace".to_string())),
497            make_token(Token::Ident("InnerWorld".to_string())),
498            make_token(Token::LBrace),
499        ];
500        let stream = PluralityTokenStream::new(&tokens);
501        assert!(stream.at_headspace_def());
502    }
503
504    #[test]
505    fn test_token_stream_cocon() {
506        let tokens = vec![
507            make_token(Token::Ident("cocon".to_string())),
508            make_token(Token::Lt),
509            make_token(Token::Ident("Stolas".to_string())),
510            make_token(Token::Comma),
511            make_token(Token::Ident("Paimon".to_string())),
512            make_token(Token::Gt),
513        ];
514        let stream = PluralityTokenStream::new(&tokens);
515        assert!(stream.at_cocon_channel());
516    }
517}