Coverage Report

Created: 2025-09-05 15:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/frontend/parser/utils.rs
Line
Count
Source
1
//! Parsing utilities and helper functions
2
3
use super::{ParserState, *};
4
use crate::frontend::ast::ImportItem;
5
6
/// Validate URL imports for safe operation
7
0
fn validate_url_import(url: &str) -> Result<()> {
8
    // Safety checks for URL imports
9
    
10
    // 1. Must be HTTPS in production (allow HTTP for local dev)
11
0
    if !url.starts_with("https://") && !url.starts_with("http://localhost") 
12
0
        && !url.starts_with("http://127.0.0.1") {
13
0
        bail!("URL imports must use HTTPS (except for localhost)");
14
0
    }
15
    
16
    // 2. Must end with .ruchy or .rchy extension
17
0
    if !url.ends_with(".ruchy") && !url.ends_with(".rchy") {
18
0
        bail!("URL imports must reference .ruchy or .rchy files");
19
0
    }
20
    
21
    // 3. Basic URL validation - no path traversal
22
0
    if url.contains("..") || url.contains("/.") {
23
0
        bail!("URL imports cannot contain path traversal sequences");
24
0
    }
25
    
26
    // 4. Disallow certain suspicious patterns
27
0
    if url.contains("javascript:") || url.contains("data:") || url.contains("file:") {
28
0
        bail!("Invalid URL scheme for import");
29
0
    }
30
    
31
0
    Ok(())
32
0
}
33
34
/// Parse a pattern for destructuring
35
///
36
/// Supports:
37
/// - Simple identifiers: `name`
38
/// - Tuple patterns: `(a, b, c)`
39
/// - List patterns: `[head, tail]`
40
/// - Struct patterns: `User { name, age }`
41
/// - Wildcard patterns: `_`
42
///
43
/// # Errors
44
///
45
/// Returns an error if the operation fails
46
47
pub fn parse_params(state: &mut ParserState) -> Result<Vec<Param>> {
47
47
    state.tokens.expect(&Token::LeftParen)
?0
;
48
49
47
    let mut params = Vec::new();
50
60
    while !
matches!49
(state.tokens.peek(), Some((Token::RightParen, _))) {
51
        // Check for mut keyword
52
49
        let is_mutable = if matches!(state.tokens.peek(), Some((Token::Mut, _))) {
53
0
            state.tokens.advance(); // consume mut
54
0
            true
55
        } else {
56
49
            false
57
        };
58
59
49
        let 
pattern48
= match state.tokens.peek() {
60
            Some((Token::Ampersand, _)) => {
61
                // Handle &self or &mut self patterns
62
0
                state.tokens.advance(); // consume &
63
64
0
                if matches!(state.tokens.peek(), Some((Token::Mut, _))) {
65
0
                    state.tokens.advance(); // consume mut
66
0
                    if let Some((Token::Identifier(n), _)) = state.tokens.peek() {
67
0
                        if n == "self" {
68
0
                            state.tokens.advance();
69
0
                            Pattern::Identifier("&mut self".to_string())
70
                        } else {
71
0
                            bail!("Expected 'self' after '&mut'");
72
                        }
73
                    } else {
74
0
                        bail!("Expected 'self' after '&mut'");
75
                    }
76
0
                } else if let Some((Token::Identifier(n), _)) = state.tokens.peek() {
77
0
                    if n == "self" {
78
0
                        state.tokens.advance();
79
0
                        Pattern::Identifier("&self".to_string())
80
                    } else {
81
0
                        bail!("Expected 'self' after '&'");
82
                    }
83
                } else {
84
0
                    bail!("Expected 'self' after '&'");
85
                }
86
            }
87
48
            Some((Token::Identifier(name), _)) => {
88
                // Only accept simple identifier patterns for parameters
89
48
                let name = name.clone();
90
48
                state.tokens.advance();
91
48
                Pattern::Identifier(name)
92
            }
93
1
            _ => bail!("Function parameters must be simple identifiers (destructuring patterns not supported)"),
94
        };
95
96
        // Type annotation is optional for gradual typing
97
48
        let ty = if 
matches!34
(state.tokens.peek(), Some((Token::Colon, _))) {
98
14
            state.tokens.advance(); // consume :
99
14
            parse_type(state)
?0
100
        } else {
101
            // Default to 'Any' type for untyped parameters
102
34
            Type {
103
34
                kind: TypeKind::Named("Any".to_string()),
104
34
                span: Span { start: 0, end: 0 },
105
34
            }
106
        };
107
108
        // Parse optional default value (only on simple identifiers)
109
48
        let default_value = if matches!(state.tokens.peek(), Some((Token::Equal, _))) {
110
0
            state.tokens.advance(); // consume =
111
0
            Some(Box::new(super::parse_expr_recursive(state)?))
112
        } else {
113
48
            None
114
        };
115
116
48
        params.push(Param {
117
48
            pattern,
118
48
            ty,
119
48
            span: Span { start: 0, end: 0 },
120
48
            is_mutable,
121
48
            default_value,
122
48
        });
123
124
48
        if 
matches!35
(state.tokens.peek(), Some((Token::Comma, _))) {
125
13
            state.tokens.advance(); // consume comma
126
13
        } else {
127
35
            break;
128
        }
129
    }
130
131
46
    state.tokens.expect(&Token::RightParen)
?0
;
132
46
    Ok(params)
133
47
}
134
135
/// # Errors
136
///
137
/// Returns an error if the operation fails
138
/// # Errors
139
///
140
/// Returns an error if the operation fails
141
3
pub fn parse_type_parameters(state: &mut ParserState) -> Result<Vec<String>> {
142
3
    state.tokens.expect(&Token::Less)
?0
;
143
144
3
    let mut type_params = Vec::new();
145
146
    // Parse first type parameter
147
3
    if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
148
3
        type_params.push(name.clone());
149
3
        state.tokens.advance();
150
3
    
}0
151
152
    // Parse additional type parameters
153
3
    while matches!(state.tokens.peek(), Some((Token::Comma, _))) {
154
0
        state.tokens.advance(); // consume comma
155
0
        if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
156
0
            type_params.push(name.clone());
157
0
            state.tokens.advance();
158
0
        }
159
    }
160
161
3
    state.tokens.expect(&Token::Greater)
?0
;
162
3
    Ok(type_params)
163
3
}
164
165
/// # Errors
166
///
167
/// Returns an error if the operation fails
168
/// # Errors
169
///
170
/// Returns an error if the operation fails
171
33
pub fn parse_type(state: &mut ParserState) -> Result<Type> {
172
33
    let span = Span { start: 0, end: 0 }; // Simplified for now
173
174
33
    match state.tokens.peek() {
175
        Some((Token::Ampersand, _)) => {
176
            // Reference type: &T or &'a T or &mut T
177
0
            state.tokens.advance(); // consume &
178
            
179
            // Check for 'mut' keyword
180
0
            let is_mut = if matches!(state.tokens.peek(), Some((Token::Mut, _))) {
181
0
                state.tokens.advance(); // consume mut
182
0
                true
183
            } else {
184
0
                false
185
            };
186
            
187
            // Parse the inner type
188
0
            let inner_type = parse_type(state)?;
189
            
190
0
            Ok(Type {
191
0
                kind: TypeKind::Reference {
192
0
                    is_mut,
193
0
                    inner: Box::new(inner_type),
194
0
                },
195
0
                span,
196
0
            })
197
        }
198
        Some((Token::Fn, _)) => {
199
            // Function type with fn keyword: fn(T1, T2) -> T3
200
0
            state.tokens.advance(); // consume fn
201
0
            state.tokens.expect(&Token::LeftParen)?;
202
            
203
0
            let mut param_types = Vec::new();
204
0
            if !matches!(state.tokens.peek(), Some((Token::RightParen, _))) {
205
0
                param_types.push(parse_type(state)?);
206
0
                while matches!(state.tokens.peek(), Some((Token::Comma, _))) {
207
0
                    state.tokens.advance(); // consume comma
208
0
                    param_types.push(parse_type(state)?);
209
                }
210
0
            }
211
            
212
0
            state.tokens.expect(&Token::RightParen)?;
213
0
            state.tokens.expect(&Token::Arrow)?;
214
0
            let ret_type = parse_type(state)?;
215
            
216
0
            Ok(Type {
217
0
                kind: TypeKind::Function {
218
0
                    params: param_types,
219
0
                    ret: Box::new(ret_type),
220
0
                },
221
0
                span,
222
0
            })
223
        }
224
        Some((Token::LeftBracket, _)) => {
225
0
            state.tokens.advance(); // consume [
226
0
            let inner = parse_type(state)?;
227
0
            state.tokens.expect(&Token::RightBracket)?;
228
            // List type: [T]
229
0
            Ok(Type {
230
0
                kind: TypeKind::List(Box::new(inner)),
231
0
                span,
232
0
            })
233
        }
234
        Some((Token::LeftParen, _)) => {
235
0
            state.tokens.advance(); // consume (
236
0
            if matches!(state.tokens.peek(), Some((Token::RightParen, _))) {
237
                // Unit type: ()
238
0
                state.tokens.advance();
239
0
                Ok(Type {
240
0
                    kind: TypeKind::Named("()".to_string()),
241
0
                    span,
242
0
                })
243
            } else {
244
                // Could be tuple type (T1, T2) or function type (T1, T2) -> T3
245
0
                let mut param_types = Vec::new();
246
0
                param_types.push(parse_type(state)?);
247
248
0
                while matches!(state.tokens.peek(), Some((Token::Comma, _))) {
249
0
                    state.tokens.advance(); // consume comma
250
0
                    param_types.push(parse_type(state)?);
251
                }
252
253
0
                state.tokens.expect(&Token::RightParen)?;
254
                
255
                // Check if this is a function type or tuple type
256
0
                if matches!(state.tokens.peek(), Some((Token::Arrow, _))) {
257
                    // Function type: (T1, T2) -> T3
258
0
                    state.tokens.advance(); // consume ->
259
0
                    let ret_type = parse_type(state)?;
260
261
0
                    Ok(Type {
262
0
                        kind: TypeKind::Function {
263
0
                            params: param_types,
264
0
                            ret: Box::new(ret_type),
265
0
                        },
266
0
                        span,
267
0
                    })
268
                } else {
269
                    // Tuple type: (T1, T2)
270
0
                    Ok(Type {
271
0
                        kind: TypeKind::Tuple(param_types),
272
0
                        span,
273
0
                    })
274
                }
275
            }
276
        }
277
33
        Some((Token::Identifier(name), _)) => {
278
33
            let mut name = name.clone();
279
33
            state.tokens.advance();
280
281
            // Check for qualified type names: std::string::String
282
33
            while matches!(state.tokens.peek(), Some((Token::ColonColon, _))) {
283
0
                state.tokens.advance(); // consume ::
284
                
285
0
                let next_name = match state.tokens.peek() {
286
0
                    Some((Token::Identifier(next), _)) => next.clone(),
287
                    // Handle special tokens that can be type names
288
0
                    Some((Token::Result, _)) => "Result".to_string(),
289
0
                    Some((Token::Option, _)) => "Option".to_string(),
290
0
                    Some((Token::Ok, _)) => "Ok".to_string(),
291
0
                    Some((Token::Err, _)) => "Err".to_string(),
292
0
                    Some((Token::Some, _)) => "Some".to_string(),
293
0
                    Some((Token::None | Token::Null, _)) => "None".to_string(),
294
0
                    _ => bail!("Expected identifier after :: in type name"),
295
                };
296
                
297
0
                name.push_str("::");
298
0
                name.push_str(&next_name);
299
0
                state.tokens.advance();
300
            }
301
302
            // Check for generic types: Vec<T>, Result<T, E>
303
33
            if matches!(state.tokens.peek(), Some((Token::Less, _))) {
304
0
                state.tokens.advance(); // consume <
305
306
0
                let mut type_params = Vec::new();
307
0
                type_params.push(parse_type(state)?);
308
309
0
                while matches!(state.tokens.peek(), Some((Token::Comma, _))) {
310
0
                    state.tokens.advance(); // consume comma
311
0
                    type_params.push(parse_type(state)?);
312
                }
313
314
0
                state.tokens.expect(&Token::Greater)?;
315
316
                // Use Generic TypeKind for parameterized types
317
0
                Ok(Type {
318
0
                    kind: TypeKind::Generic {
319
0
                        base: name,
320
0
                        params: type_params,
321
0
                    },
322
0
                    span,
323
0
                })
324
            } else {
325
33
                Ok(Type {
326
33
                    kind: TypeKind::Named(name),
327
33
                    span,
328
33
                })
329
            }
330
        }
331
0
        _ => bail!("Expected type"),
332
    }
333
33
}
334
335
/// Parse import statements in various forms
336
///
337
/// Supports:
338
/// - Simple imports: `import std::collections::HashMap`
339
/// - Multiple imports: `import std::io::{Read, Write}`
340
/// - Aliased imports: `import std::collections::{HashMap as Map}`
341
/// - Wildcard imports: `import std::collections::*`
342
///
343
/// # Examples
344
///
345
/// ```
346
/// use ruchy::frontend::parser::Parser;
347
/// use ruchy::frontend::ast::{ExprKind, ImportItem};
348
///
349
/// let mut parser = Parser::new("import std::collections::HashMap");
350
/// let expr = parser.parse().unwrap();
351
///
352
/// match &expr.kind {
353
///     ExprKind::Import { path, items } => {
354
///         assert_eq!(path, "std::collections::HashMap");
355
///         assert_eq!(items.len(), 1);
356
///         assert!(matches!(items[0], ImportItem::Named(ref name) if name == "HashMap"));
357
///     }
358
///     _ => panic!("Expected Import expression"),
359
/// }
360
/// ```
361
///
362
/// ```
363
/// use ruchy::frontend::parser::Parser;
364
/// use ruchy::frontend::ast::{ExprKind, ImportItem};
365
///
366
/// // Multiple imports with alias
367
/// let mut parser = Parser::new("import std::collections::{HashMap as Map, Vec}");
368
/// let expr = parser.parse().unwrap();
369
///
370
/// match &expr.kind {
371
///     ExprKind::Import { path, items } => {
372
///         assert_eq!(path, "std::collections");
373
///         assert_eq!(items.len(), 2);
374
///         assert!(matches!(&items[0], ImportItem::Aliased { name, alias }
375
///                          if name == "HashMap" && alias == "Map"));
376
///         assert!(matches!(&items[1], ImportItem::Named(name) if name == "Vec"));
377
///     }
378
///     _ => panic!("Expected Import expression"),
379
/// }
380
/// ```
381
///
382
/// # Errors
383
///
384
/// Returns an error if:
385
/// - No identifier follows the import keyword
386
/// - Invalid syntax in import specification
387
/// - Unexpected tokens in import list
388
0
pub fn parse_import(state: &mut ParserState) -> Result<Expr> {
389
0
    let start_span = state.tokens.advance().expect("checked by parser logic").1; // consume import/use
390
391
0
    let mut path_parts = Vec::new();
392
393
    // Check for URL import (e.g., import "https://example.com/module.ruchy")
394
0
    if let Some((Token::String(url), _)) = state.tokens.peek() {
395
        // URL import - validate it starts with https://
396
0
        if !url.starts_with("https://") && !url.starts_with("http://") {
397
0
            bail!("URL imports must start with 'https://' or 'http://'");
398
0
        }
399
        
400
        // Safety validation for URL imports
401
0
        validate_url_import(url)?;
402
        
403
0
        let url = url.clone();
404
0
        state.tokens.advance();
405
        
406
        // URL imports are always single module imports (no wildcard or specific items)
407
0
        let span = start_span; // simplified for now
408
0
        return Ok(Expr::new(
409
0
            ExprKind::Import {
410
0
                path: url,
411
0
                items: vec![ImportItem::Named("*".to_string())], // Import all from URL
412
0
            },
413
0
            span,
414
0
        ));
415
0
    }
416
    
417
    // Parse regular module path (e.g., std::io::prelude)
418
0
    if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
419
0
        path_parts.push(name.clone());
420
0
        state.tokens.advance();
421
422
0
        while matches!(state.tokens.peek(), Some((Token::ColonColon, _))) {
423
            // Check for ::
424
0
            state.tokens.advance(); // consume ::
425
426
            // Check for wildcard or brace after ::
427
0
            if matches!(
428
0
                state.tokens.peek(),
429
                Some((Token::Star | Token::LeftBrace, _))
430
            ) {
431
                // This is the start of import items, break out of path parsing
432
0
                break;
433
0
            }
434
435
0
            if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
436
0
                path_parts.push(name.clone());
437
0
                state.tokens.advance();
438
0
            } else {
439
0
                bail!("Expected identifier, '*', or '{{' after '::'");
440
            }
441
        }
442
0
    }
443
444
    // Check for specific imports like ::{Read, Write} or ::*
445
    // Note: We may have already consumed the :: in the loop above
446
0
    let items = if matches!(state.tokens.peek(), Some((Token::Star, _))) {
447
        // Wildcard import: import path::*
448
0
        state.tokens.advance(); // consume *
449
0
        vec![ImportItem::Wildcard]
450
0
    } else if matches!(state.tokens.peek(), Some((Token::LeftBrace, _))) {
451
        // Specific imports: import path::{item1, item2, ...}
452
0
        state.tokens.expect(&Token::LeftBrace)?; // consume {
453
454
0
        let mut items = Vec::new();
455
0
        while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) {
456
0
            if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
457
0
                let name = name.clone();
458
0
                state.tokens.advance();
459
460
                // Check for alias: item as alias
461
0
                if matches!(state.tokens.peek(), Some((Token::As, _))) {
462
0
                    state.tokens.advance(); // consume as
463
0
                    if let Some((Token::Identifier(alias), _)) = state.tokens.peek() {
464
0
                        let alias = alias.clone();
465
0
                        state.tokens.advance();
466
0
                        items.push(ImportItem::Aliased { name, alias });
467
0
                    } else {
468
0
                        bail!("Expected alias name after 'as'");
469
                    }
470
0
                } else {
471
0
                    items.push(ImportItem::Named(name));
472
0
                }
473
474
0
                if matches!(state.tokens.peek(), Some((Token::Comma, _))) {
475
0
                    state.tokens.advance();
476
0
                    // After comma, continue to parse next item
477
0
                    // Don't break here - continue the loop
478
0
                } else if !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) {
479
                    // If not comma and not right brace, error
480
0
                    bail!("Expected ',' or '}}' in import list");
481
0
                }
482
0
            } else if !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) {
483
0
                bail!("Expected identifier or '}}' in import list");
484
0
            }
485
        }
486
487
0
        state.tokens.expect(&Token::RightBrace)?;
488
0
        items
489
    } else {
490
        // Simple import: import path or import path as alias
491
0
        if matches!(state.tokens.peek(), Some((Token::As, _))) {
492
            // import path as alias
493
0
            state.tokens.advance(); // consume as
494
0
            if let Some((Token::Identifier(alias), _)) = state.tokens.peek() {
495
0
                let alias = alias.clone();
496
0
                state.tokens.advance();
497
0
                vec![ImportItem::Aliased {
498
0
                    name: path_parts.last().unwrap_or(&String::new()).clone(),
499
0
                    alias,
500
0
                }]
501
            } else {
502
0
                bail!("Expected alias name after 'as'");
503
            }
504
        } else {
505
            // Simple import without alias
506
0
            if path_parts.is_empty() {
507
0
                Vec::new()
508
0
            } else if path_parts.len() == 1 {
509
                // Single segment like "use math;" - treat as wildcard (use math::*)
510
0
                Vec::new() // Empty items = wildcard import in transpiler
511
            } else {
512
                // Multi-segment like "use std::collections::HashMap;" - import the last part
513
0
                vec![ImportItem::Named(
514
0
                    path_parts
515
0
                        .last()
516
0
                        .expect("checked: !path_parts.is_empty()")
517
0
                        .clone(),
518
0
                )]
519
            }
520
        }
521
    };
522
523
0
    let path = path_parts.join("::");
524
525
    // Validate that we have either a path or items (or both)
526
0
    if path.is_empty() && items.is_empty() {
527
0
        bail!("Expected import path or items after 'import'");
528
0
    }
529
530
0
    let span = start_span; // simplified for now
531
532
0
    Ok(Expr::new(ExprKind::Import { path, items }, span))
533
0
}
534
535
/// # Errors
536
///
537
/// Returns an error if the operation fails
538
/// # Errors
539
///
540
/// Returns an error if the operation fails
541
412
pub fn parse_attributes(state: &mut ParserState) -> Result<Vec<Attribute>> {
542
412
    let mut attributes = Vec::new();
543
544
412
    while matches!(state.tokens.peek(), Some((Token::Hash, _))) {
545
0
        state.tokens.advance(); // consume #
546
547
0
        if !matches!(state.tokens.peek(), Some((Token::LeftBracket, _))) {
548
0
            bail!("Expected '[' after '#'");
549
0
        }
550
0
        state.tokens.advance(); // consume [
551
552
0
        let name = if let Some((Token::Identifier(n), _)) = state.tokens.peek() {
553
0
            let name = n.clone();
554
0
            state.tokens.advance();
555
0
            name
556
        } else {
557
0
            bail!("Expected attribute name");
558
        };
559
560
0
        let mut args = Vec::new();
561
0
        if matches!(state.tokens.peek(), Some((Token::LeftParen, _))) {
562
0
            state.tokens.advance(); // consume (
563
564
0
            while !matches!(state.tokens.peek(), Some((Token::RightParen, _))) {
565
0
                if let Some((Token::Identifier(arg), _)) = state.tokens.peek() {
566
0
                    args.push(arg.clone());
567
0
                    state.tokens.advance();
568
569
0
                    if matches!(state.tokens.peek(), Some((Token::Comma, _))) {
570
0
                        state.tokens.advance();
571
0
                    } else {
572
0
                        break;
573
                    }
574
                } else {
575
0
                    break;
576
                }
577
            }
578
579
0
            state.tokens.advance(); // consume )
580
0
        }
581
582
0
        let end_span = state.tokens.advance().expect("Expected ']' token").1; // consume ]
583
584
0
        attributes.push(Attribute {
585
0
            name,
586
0
            args,
587
0
            span: end_span,
588
0
        });
589
    }
590
591
412
    Ok(attributes)
592
412
}
593
594
/// Parse string interpolation from a string containing {expr} patterns
595
0
pub fn parse_string_interpolation(_state: &mut ParserState, s: &str) -> Vec<StringPart> {
596
0
    let mut parts = Vec::new();
597
0
    let mut chars = s.chars().peekable();
598
0
    let mut current_text = String::new();
599
600
0
    while let Some(ch) = chars.next() {
601
0
        match ch {
602
0
            '{' if chars.peek() == Some(&'{') => {
603
0
                // Escaped brace: {{
604
0
                chars.next(); // consume second '{'
605
0
                current_text.push('{');
606
0
            }
607
0
            '}' if chars.peek() == Some(&'}') => {
608
0
                // Escaped brace: }}
609
0
                chars.next(); // consume second '}'
610
0
                current_text.push('}');
611
0
            }
612
            '{' => {
613
                // Start of interpolation
614
0
                if !current_text.is_empty() {
615
0
                    parts.push(StringPart::Text(current_text.clone()));
616
0
                    current_text.clear();
617
0
                }
618
619
                // Collect expression until closing '}' with proper string literal handling
620
0
                let mut expr_text = String::new();
621
0
                let mut brace_count = 1;
622
0
                let mut in_string = false;
623
0
                let mut in_char = false;
624
0
                let mut escaped = false;
625
626
0
                for expr_ch in chars.by_ref() {
627
0
                    match expr_ch {
628
0
                        '"' if !in_char && !escaped => {
629
0
                            in_string = !in_string;
630
0
                            expr_text.push(expr_ch);
631
0
                        }
632
0
                        '\'' if !in_string && !escaped => {
633
0
                            in_char = !in_char;
634
0
                            expr_text.push(expr_ch);
635
0
                        }
636
0
                        '{' if !in_string && !in_char => {
637
0
                            brace_count += 1;
638
0
                            expr_text.push(expr_ch);
639
0
                        }
640
0
                        '}' if !in_string && !in_char => {
641
0
                            brace_count -= 1;
642
0
                            if brace_count == 0 {
643
0
                                break;
644
0
                            }
645
0
                            expr_text.push(expr_ch);
646
                        }
647
0
                        '\\' if (in_string || in_char) && !escaped => {
648
0
                            escaped = true;
649
0
                            expr_text.push(expr_ch);
650
0
                        }
651
0
                        _ => {
652
0
                            escaped = false;
653
0
                            expr_text.push(expr_ch);
654
0
                        }
655
                    }
656
657
                    // Reset escape flag for non-backslash characters
658
0
                    if expr_ch != '\\' {
659
0
                        escaped = false;
660
0
                    }
661
                }
662
663
                // Check if there's a format specifier (e.g., "score:.2" -> "score" and ":.2")
664
0
                let (expr_part, format_spec) = if let Some(colon_pos) = expr_text.find(':') {
665
                    // Check if the colon is not inside a string or character literal
666
                    // Simple heuristic: if there are no quotes before the colon, it's likely a format spec
667
0
                    let before_colon = &expr_text[..colon_pos];
668
0
                    if !before_colon.contains('"') && !before_colon.contains('\'') {
669
0
                        (&expr_text[..colon_pos], Some(&expr_text[colon_pos..]))
670
                    } else {
671
0
                        (expr_text.as_str(), None)
672
                    }
673
                } else {
674
0
                    (expr_text.as_str(), None)
675
                };
676
                
677
                // Parse the expression part (without format specifier)
678
0
                let mut expr_parser = super::core::Parser::new(expr_part);
679
0
                match expr_parser.parse() {
680
0
                    Ok(expr) => {
681
                        // Store the expression with or without format specifier
682
0
                        if let Some(spec) = format_spec {
683
0
                            parts.push(StringPart::ExprWithFormat {
684
0
                                expr: Box::new(expr),
685
0
                                format_spec: spec.to_string(),
686
0
                            });
687
0
                        } else {
688
0
                            parts.push(StringPart::Expr(Box::new(expr)));
689
0
                        }
690
                    }
691
0
                    Err(_) => {
692
0
                        // Fallback to text if parsing fails
693
0
                        parts.push(StringPart::Text(format!("{{{expr_text}}}")));
694
0
                    }
695
                }
696
            }
697
0
            _ => current_text.push(ch),
698
        }
699
    }
700
701
    // Add remaining text
702
0
    if !current_text.is_empty() {
703
0
        parts.push(StringPart::Text(current_text));
704
0
    }
705
706
0
    parts
707
0
}
708
709
/// Parse module declarations
710
///
711
/// Supports:
712
/// - Empty modules: `module MyModule {}`
713
/// - Single expression modules: `module Math { sqrt(x) }`
714
/// - Multi-expression modules: `module Utils { fn helper() {...}; const PI = 3.14 }`
715
///
716
/// # Examples
717
///
718
/// ```
719
/// use ruchy::frontend::parser::Parser;
720
/// use ruchy::frontend::ast::ExprKind;
721
///
722
/// // Empty module
723
/// let mut parser = Parser::new("module Empty {}");
724
/// let expr = parser.parse().unwrap();
725
///
726
/// match &expr.kind {
727
///     ExprKind::Module { name, .. } => {
728
///         assert_eq!(name, "Empty");
729
///     }
730
///     _ => panic!("Expected Module expression"),
731
/// }
732
/// ```
733
///
734
/// ```
735
/// use ruchy::frontend::parser::Parser;
736
/// use ruchy::frontend::ast::{ExprKind, Literal};
737
///
738
/// // Module with content
739
/// let mut parser = Parser::new("module Math { 42 }");
740
/// let expr = parser.parse().unwrap();
741
///
742
/// match &expr.kind {
743
///     ExprKind::Module { name, body } => {
744
///         assert_eq!(name, "Math");
745
///         // Verify body contains literal 42
746
///         match &body.kind {
747
///             ExprKind::Literal(Literal::Integer(n)) => assert_eq!(*n, 42),
748
///             _ => panic!("Expected integer literal in module body"),
749
///         }
750
///     }
751
///     _ => panic!("Expected Module expression"),
752
/// }
753
/// ```
754
///
755
/// # Errors
756
///
757
/// Returns an error if:
758
/// - No identifier follows the module keyword
759
/// - Missing opening or closing braces
760
/// - Invalid syntax in module body
761
0
pub fn parse_module(state: &mut ParserState) -> Result<Expr> {
762
0
    let start_span = state.tokens.advance().expect("checked by parser logic").1; // consume module
763
764
    // Parse module name
765
0
    let name = if let Some((Token::Identifier(n), _)) = state.tokens.peek() {
766
0
        let name = n.clone();
767
0
        state.tokens.advance();
768
0
        name
769
    } else {
770
0
        bail!("Expected module name after 'module'");
771
    };
772
773
    // Expect opening brace
774
0
    state.tokens.expect(&Token::LeftBrace)?;
775
776
    // Parse module body (can be a block or single expression)
777
0
    let body = if matches!(state.tokens.peek(), Some((Token::RightBrace, _))) {
778
        // Empty module
779
0
        Box::new(Expr::new(
780
0
            ExprKind::Literal(Literal::Unit),
781
0
            Span { start: 0, end: 0 },
782
        ))
783
    } else {
784
        // Parse expressions until we hit the closing brace
785
0
        let mut exprs = Vec::new();
786
0
        while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) {
787
0
            exprs.push(super::parse_expr_recursive(state)?);
788
            // Optional semicolon or comma separator
789
0
            if matches!(
790
0
                state.tokens.peek(),
791
                Some((Token::Semicolon | Token::Comma, _))
792
0
            ) {
793
0
                state.tokens.advance();
794
0
            }
795
        }
796
797
0
        if exprs.len() == 1 {
798
0
            Box::new(exprs.into_iter().next().expect("checked: exprs.len() == 1"))
799
        } else {
800
0
            Box::new(Expr::new(ExprKind::Block(exprs), Span { start: 0, end: 0 }))
801
        }
802
    };
803
804
    // Expect closing brace
805
0
    state.tokens.expect(&Token::RightBrace)?;
806
807
0
    Ok(Expr::new(ExprKind::Module { name, body }, start_span))
808
0
}
809
810
/// Parse export statements
811
///
812
/// Supports:
813
/// - Single exports: `export myFunction`
814
/// - Multiple exports: `export { func1, func2, func3 }`
815
///
816
/// # Examples
817
///
818
/// ```
819
/// use ruchy::frontend::parser::Parser;
820
/// use ruchy::frontend::ast::ExprKind;
821
///
822
/// // Single export
823
/// let mut parser = Parser::new("export myFunction");
824
/// let expr = parser.parse().unwrap();
825
///
826
/// match &expr.kind {
827
///     ExprKind::Export { items } => {
828
///         assert_eq!(items.len(), 1);
829
///         assert_eq!(items[0], "myFunction");
830
///     }
831
///     _ => panic!("Expected Export expression"),
832
/// }
833
/// ```
834
///
835
/// ```
836
/// use ruchy::frontend::parser::Parser;
837
/// use ruchy::frontend::ast::ExprKind;
838
///
839
/// // Multiple exports
840
/// let mut parser = Parser::new("export { add, subtract, multiply }");
841
/// let expr = parser.parse().unwrap();
842
///
843
/// match &expr.kind {
844
///     ExprKind::Export { items } => {
845
///         assert_eq!(items.len(), 3);
846
///         assert!(items.contains(&"add".to_string()));
847
///         assert!(items.contains(&"subtract".to_string()));
848
///         assert!(items.contains(&"multiply".to_string()));
849
///     }
850
///     _ => panic!("Expected Export expression"),
851
/// }
852
/// ```
853
///
854
/// # Errors
855
///
856
/// Returns an error if:
857
/// - No identifier or brace follows the export keyword
858
/// - Invalid syntax in export list
859
/// - Missing closing brace in export block
860
0
pub fn parse_export(state: &mut ParserState) -> Result<Expr> {
861
0
    let start_span = state.tokens.advance().expect("checked by parser logic").1; // consume export
862
863
0
    let mut items = Vec::new();
864
865
    // Parse export list
866
0
    if matches!(state.tokens.peek(), Some((Token::LeftBrace, _))) {
867
        // Export block: export { item1, item2, ... }
868
0
        state.tokens.advance(); // consume {
869
870
0
        while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) {
871
0
            if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
872
0
                items.push(name.clone());
873
0
                state.tokens.advance();
874
875
0
                if matches!(state.tokens.peek(), Some((Token::Comma, _))) {
876
0
                    state.tokens.advance(); // consume comma
877
0
                } else {
878
0
                    break;
879
                }
880
            } else {
881
0
                bail!("Expected identifier in export list");
882
            }
883
        }
884
885
0
        state.tokens.expect(&Token::RightBrace)?;
886
0
    } else if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
887
0
        // Single export: export item
888
0
        items.push(name.clone());
889
0
        state.tokens.advance();
890
0
    } else {
891
0
        bail!("Expected export list or identifier after 'export'");
892
    }
893
894
0
    Ok(Expr::new(ExprKind::Export { items }, start_span))
895
0
}