/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 | 0 | pub fn parse_params(state: &mut ParserState) -> Result<Vec<Param>> { |
47 | 0 | state.tokens.expect(&Token::LeftParen)?; |
48 | | |
49 | 0 | let mut params = Vec::new(); |
50 | 0 | while !matches!(state.tokens.peek(), Some((Token::RightParen, _))) { |
51 | 0 | let param = parse_single_param(state)?; |
52 | 0 | params.push(param); |
53 | | |
54 | 0 | if !should_continue_param_list(state)? { |
55 | 0 | break; |
56 | 0 | } |
57 | | } |
58 | | |
59 | 0 | state.tokens.expect(&Token::RightParen)?; |
60 | 0 | Ok(params) |
61 | 0 | } |
62 | | |
63 | | /// Parse a single parameter (complexity: 7) |
64 | 0 | fn parse_single_param(state: &mut ParserState) -> Result<Param> { |
65 | 0 | let is_mutable = check_and_consume_mut(state); |
66 | 0 | let pattern = parse_param_pattern(state)?; |
67 | 0 | let ty = parse_optional_type_annotation(state)?; |
68 | 0 | let default_value = parse_optional_default_value(state)?; |
69 | | |
70 | 0 | Ok(Param { |
71 | 0 | pattern, |
72 | 0 | ty, |
73 | 0 | span: Span { start: 0, end: 0 }, |
74 | 0 | is_mutable, |
75 | 0 | default_value, |
76 | 0 | }) |
77 | 0 | } |
78 | | |
79 | | /// Check for and consume mut keyword (complexity: 2) |
80 | 0 | fn check_and_consume_mut(state: &mut ParserState) -> bool { |
81 | 0 | if matches!(state.tokens.peek(), Some((Token::Mut, _))) { |
82 | 0 | state.tokens.advance(); |
83 | 0 | true |
84 | | } else { |
85 | 0 | false |
86 | | } |
87 | 0 | } |
88 | | |
89 | | /// Parse parameter pattern (complexity: 8 - increased to support destructuring) |
90 | 0 | fn parse_param_pattern(state: &mut ParserState) -> Result<Pattern> { |
91 | 0 | match state.tokens.peek() { |
92 | 0 | Some((Token::Ampersand, _)) => parse_reference_pattern(state), |
93 | 0 | Some((Token::Identifier(name), _)) => { |
94 | 0 | let name = name.clone(); |
95 | 0 | state.tokens.advance(); |
96 | 0 | Ok(Pattern::Identifier(name)) |
97 | | } |
98 | | Some((Token::DataFrame, _)) => { |
99 | | // Handle "df" parameter name (tokenized as DataFrame) |
100 | 0 | state.tokens.advance(); |
101 | 0 | Ok(Pattern::Identifier("df".to_string())) |
102 | | } |
103 | | Some((Token::LeftParen, _)) => { |
104 | | // Parse tuple destructuring: fun f((x, y)) {} |
105 | 0 | super::expressions::parse_tuple_pattern(state) |
106 | | } |
107 | | Some((Token::LeftBracket, _)) => { |
108 | | // Parse list destructuring: fun f([x, y]) {} |
109 | 0 | super::expressions::parse_list_pattern(state) |
110 | | } |
111 | | Some((Token::LeftBrace, _)) => { |
112 | | // Parse struct destructuring: fun f({x, y}) {} |
113 | 0 | super::expressions::parse_struct_pattern(state) |
114 | | } |
115 | 0 | _ => bail!("Function parameters must be simple identifiers or destructuring patterns"), |
116 | | } |
117 | 0 | } |
118 | | |
119 | | /// Parse reference patterns (&self, &mut self) (complexity: 8) |
120 | 0 | fn parse_reference_pattern(state: &mut ParserState) -> Result<Pattern> { |
121 | 0 | state.tokens.advance(); // consume & |
122 | | |
123 | 0 | let is_mut_ref = matches!(state.tokens.peek(), Some((Token::Mut, _))); |
124 | 0 | if is_mut_ref { |
125 | 0 | state.tokens.advance(); // consume mut |
126 | 0 | } |
127 | | |
128 | 0 | match state.tokens.peek() { |
129 | 0 | Some((Token::Identifier(n), _)) if n == "self" => { |
130 | 0 | state.tokens.advance(); |
131 | 0 | if is_mut_ref { |
132 | 0 | Ok(Pattern::Identifier("&mut self".to_string())) |
133 | | } else { |
134 | 0 | Ok(Pattern::Identifier("&self".to_string())) |
135 | | } |
136 | | } |
137 | | _ => { |
138 | 0 | let expected = if is_mut_ref { "'self' after '&mut'" } else { "'self' after '&'" }; |
139 | 0 | bail!("Expected {}", expected) |
140 | | } |
141 | | } |
142 | 0 | } |
143 | | |
144 | | /// Parse optional type annotation (complexity: 4) |
145 | 0 | fn parse_optional_type_annotation(state: &mut ParserState) -> Result<Type> { |
146 | 0 | if matches!(state.tokens.peek(), Some((Token::Colon, _))) { |
147 | 0 | state.tokens.advance(); // consume : |
148 | 0 | parse_type(state) |
149 | | } else { |
150 | | // Default to 'Any' type for untyped parameters |
151 | 0 | Ok(Type { |
152 | 0 | kind: TypeKind::Named("Any".to_string()), |
153 | 0 | span: Span { start: 0, end: 0 }, |
154 | 0 | }) |
155 | | } |
156 | 0 | } |
157 | | |
158 | | /// Parse optional default value (complexity: 3) |
159 | 0 | fn parse_optional_default_value(state: &mut ParserState) -> Result<Option<Box<Expr>>> { |
160 | 0 | if matches!(state.tokens.peek(), Some((Token::Equal, _))) { |
161 | 0 | state.tokens.advance(); // consume = |
162 | 0 | Ok(Some(Box::new(super::parse_expr_recursive(state)?))) |
163 | | } else { |
164 | 0 | Ok(None) |
165 | | } |
166 | 0 | } |
167 | | |
168 | | /// Check if we should continue parsing parameters (complexity: 3) |
169 | 0 | fn should_continue_param_list(state: &mut ParserState) -> Result<bool> { |
170 | 0 | if matches!(state.tokens.peek(), Some((Token::Comma, _))) { |
171 | 0 | state.tokens.advance(); // consume comma |
172 | 0 | Ok(true) |
173 | | } else { |
174 | 0 | Ok(false) |
175 | | } |
176 | 0 | } |
177 | | |
178 | | /// # Errors |
179 | | /// |
180 | | /// Returns an error if the operation fails |
181 | | /// # Errors |
182 | | /// |
183 | | /// Returns an error if the operation fails |
184 | 0 | pub fn parse_type_parameters(state: &mut ParserState) -> Result<Vec<String>> { |
185 | 0 | state.tokens.expect(&Token::Less)?; |
186 | | |
187 | 0 | let mut type_params = Vec::new(); |
188 | | |
189 | | // Parse first type parameter |
190 | 0 | if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
191 | 0 | type_params.push(name.clone()); |
192 | 0 | state.tokens.advance(); |
193 | 0 | } |
194 | | |
195 | | // Parse additional type parameters |
196 | 0 | while matches!(state.tokens.peek(), Some((Token::Comma, _))) { |
197 | 0 | state.tokens.advance(); // consume comma |
198 | 0 | if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
199 | 0 | type_params.push(name.clone()); |
200 | 0 | state.tokens.advance(); |
201 | 0 | } |
202 | | } |
203 | | |
204 | 0 | state.tokens.expect(&Token::Greater)?; |
205 | 0 | Ok(type_params) |
206 | 0 | } |
207 | | |
208 | | /// Parse type expressions with complexity ≤10 |
209 | | /// # Errors |
210 | | /// Returns an error if the operation fails |
211 | 0 | pub fn parse_type(state: &mut ParserState) -> Result<Type> { |
212 | 0 | let span = Span { start: 0, end: 0 }; // Simplified for now |
213 | | |
214 | 0 | match state.tokens.peek() { |
215 | 0 | Some((Token::Ampersand, _)) => parse_reference_type(state, span), |
216 | 0 | Some((Token::Fn, _)) => parse_fn_type(state, span), |
217 | 0 | Some((Token::LeftBracket, _)) => parse_list_type(state, span), |
218 | 0 | Some((Token::LeftParen, _)) => parse_paren_type(state, span), |
219 | | Some((Token::Identifier(_) | Token::Result | Token::Option | Token::Ok | |
220 | 0 | Token::Err | Token::Some | Token::DataFrame | Token::None | Token::Null, _)) => parse_named_type(state, span), |
221 | 0 | _ => bail!("Expected type"), |
222 | | } |
223 | 0 | } |
224 | | |
225 | | // Helper: Parse reference type &T or &mut T (complexity: 4) |
226 | 0 | fn parse_reference_type(state: &mut ParserState, span: Span) -> Result<Type> { |
227 | 0 | state.tokens.advance(); // consume & |
228 | | |
229 | 0 | let is_mut = if matches!(state.tokens.peek(), Some((Token::Mut, _))) { |
230 | 0 | state.tokens.advance(); // consume mut |
231 | 0 | true |
232 | | } else { |
233 | 0 | false |
234 | | }; |
235 | | |
236 | 0 | let inner_type = parse_type(state)?; |
237 | | |
238 | 0 | Ok(Type { |
239 | 0 | kind: TypeKind::Reference { |
240 | 0 | is_mut, |
241 | 0 | inner: Box::new(inner_type), |
242 | 0 | }, |
243 | 0 | span, |
244 | 0 | }) |
245 | 0 | } |
246 | | |
247 | | // Helper: Parse function type fn(T1, T2) -> T3 (complexity: 5) |
248 | 0 | fn parse_fn_type(state: &mut ParserState, span: Span) -> Result<Type> { |
249 | 0 | state.tokens.advance(); // consume fn |
250 | 0 | state.tokens.expect(&Token::LeftParen)?; |
251 | | |
252 | 0 | let param_types = parse_type_list(state)?; |
253 | 0 | state.tokens.expect(&Token::RightParen)?; |
254 | 0 | state.tokens.expect(&Token::Arrow)?; |
255 | 0 | let ret_type = parse_type(state)?; |
256 | | |
257 | 0 | Ok(Type { |
258 | 0 | kind: TypeKind::Function { |
259 | 0 | params: param_types, |
260 | 0 | ret: Box::new(ret_type), |
261 | 0 | }, |
262 | 0 | span, |
263 | 0 | }) |
264 | 0 | } |
265 | | |
266 | | // Helper: Parse list type `[T]` (complexity: 3) |
267 | 0 | fn parse_list_type(state: &mut ParserState, span: Span) -> Result<Type> { |
268 | 0 | state.tokens.advance(); // consume [ |
269 | 0 | let inner = parse_type(state)?; |
270 | 0 | state.tokens.expect(&Token::RightBracket)?; |
271 | | |
272 | 0 | Ok(Type { |
273 | 0 | kind: TypeKind::List(Box::new(inner)), |
274 | 0 | span, |
275 | 0 | }) |
276 | 0 | } |
277 | | |
278 | | // Helper: Parse parenthesized type (T1, T2) or (T1, T2) -> T3 (complexity: 6) |
279 | 0 | fn parse_paren_type(state: &mut ParserState, span: Span) -> Result<Type> { |
280 | 0 | state.tokens.advance(); // consume ( |
281 | | |
282 | 0 | if matches!(state.tokens.peek(), Some((Token::RightParen, _))) { |
283 | | // Unit type: () |
284 | 0 | state.tokens.advance(); |
285 | 0 | Ok(Type { |
286 | 0 | kind: TypeKind::Named("()".to_string()), |
287 | 0 | span, |
288 | 0 | }) |
289 | | } else { |
290 | 0 | let param_types = parse_type_list(state)?; |
291 | 0 | state.tokens.expect(&Token::RightParen)?; |
292 | | |
293 | 0 | if matches!(state.tokens.peek(), Some((Token::Arrow, _))) { |
294 | | // Function type: (T1, T2) -> T3 |
295 | 0 | state.tokens.advance(); // consume -> |
296 | 0 | let ret_type = parse_type(state)?; |
297 | | |
298 | 0 | Ok(Type { |
299 | 0 | kind: TypeKind::Function { |
300 | 0 | params: param_types, |
301 | 0 | ret: Box::new(ret_type), |
302 | 0 | }, |
303 | 0 | span, |
304 | 0 | }) |
305 | | } else { |
306 | | // Tuple type: (T1, T2) |
307 | 0 | Ok(Type { |
308 | 0 | kind: TypeKind::Tuple(param_types), |
309 | 0 | span, |
310 | 0 | }) |
311 | | } |
312 | | } |
313 | 0 | } |
314 | | |
315 | | // Helper: Parse named type with optional generics (complexity: 4) |
316 | 0 | fn parse_named_type(state: &mut ParserState, span: Span) -> Result<Type> { |
317 | 0 | let name = parse_qualified_name(state)?; |
318 | | |
319 | 0 | if matches!(state.tokens.peek(), Some((Token::Less, _))) { |
320 | 0 | parse_generic_type(state, name, span) |
321 | | } else { |
322 | 0 | Ok(Type { |
323 | 0 | kind: TypeKind::Named(name), |
324 | 0 | span, |
325 | 0 | }) |
326 | | } |
327 | 0 | } |
328 | | |
329 | | // Helper: Parse qualified name like std::collections::HashMap (complexity: 6) |
330 | 0 | fn parse_qualified_name(state: &mut ParserState) -> Result<String> { |
331 | 0 | let mut name = match state.tokens.peek() { |
332 | 0 | Some((Token::Identifier(n), _)) => { |
333 | 0 | let name = n.clone(); |
334 | 0 | state.tokens.advance(); |
335 | 0 | name |
336 | | } |
337 | | // Handle special tokens that can be type names |
338 | | Some((Token::Result, _)) => { |
339 | 0 | state.tokens.advance(); |
340 | 0 | "Result".to_string() |
341 | | } |
342 | | Some((Token::Option, _)) => { |
343 | 0 | state.tokens.advance(); |
344 | 0 | "Option".to_string() |
345 | | } |
346 | | Some((Token::Ok, _)) => { |
347 | 0 | state.tokens.advance(); |
348 | 0 | "Ok".to_string() |
349 | | } |
350 | | Some((Token::Err, _)) => { |
351 | 0 | state.tokens.advance(); |
352 | 0 | "Err".to_string() |
353 | | } |
354 | | Some((Token::Some, _)) => { |
355 | 0 | state.tokens.advance(); |
356 | 0 | "Some".to_string() |
357 | | } |
358 | | Some((Token::DataFrame, _)) => { |
359 | 0 | state.tokens.advance(); |
360 | 0 | "DataFrame".to_string() |
361 | | } |
362 | | Some((Token::None | Token::Null, _)) => { |
363 | 0 | state.tokens.advance(); |
364 | 0 | "None".to_string() |
365 | | } |
366 | 0 | _ => bail!("Expected identifier"), |
367 | | }; |
368 | | |
369 | 0 | while matches!(state.tokens.peek(), Some((Token::ColonColon, _))) { |
370 | 0 | state.tokens.advance(); // consume :: |
371 | | |
372 | 0 | let next_name = match state.tokens.peek() { |
373 | 0 | Some((Token::Identifier(next), _)) => next.clone(), |
374 | | // Handle special tokens that can be type names |
375 | 0 | Some((Token::Result, _)) => "Result".to_string(), |
376 | 0 | Some((Token::Option, _)) => "Option".to_string(), |
377 | 0 | Some((Token::Ok, _)) => "Ok".to_string(), |
378 | 0 | Some((Token::Err, _)) => "Err".to_string(), |
379 | 0 | Some((Token::Some, _)) => "Some".to_string(), |
380 | 0 | Some((Token::None | Token::Null, _)) => "None".to_string(), |
381 | 0 | _ => bail!("Expected identifier after :: in type name"), |
382 | | }; |
383 | | |
384 | 0 | name.push_str("::"); |
385 | 0 | name.push_str(&next_name); |
386 | 0 | state.tokens.advance(); |
387 | | } |
388 | | |
389 | 0 | Ok(name) |
390 | 0 | } |
391 | | |
392 | | // Helper: Parse generic type Vec<T, U> (complexity: 4) |
393 | 0 | fn parse_generic_type(state: &mut ParserState, base: String, span: Span) -> Result<Type> { |
394 | 0 | state.tokens.advance(); // consume < |
395 | 0 | let type_params = parse_type_list(state)?; |
396 | 0 | state.tokens.expect(&Token::Greater)?; |
397 | | |
398 | 0 | Ok(Type { |
399 | 0 | kind: TypeKind::Generic { |
400 | 0 | base, |
401 | 0 | params: type_params, |
402 | 0 | }, |
403 | 0 | span, |
404 | 0 | }) |
405 | 0 | } |
406 | | |
407 | | // Helper: Parse comma-separated type list (complexity: 3) |
408 | 0 | fn parse_type_list(state: &mut ParserState) -> Result<Vec<Type>> { |
409 | 0 | let mut types = Vec::new(); |
410 | | |
411 | 0 | if !matches!(state.tokens.peek(), Some((Token::RightParen | Token::Greater, _))) { |
412 | 0 | types.push(parse_type(state)?); |
413 | | |
414 | 0 | while matches!(state.tokens.peek(), Some((Token::Comma, _))) { |
415 | 0 | state.tokens.advance(); // consume comma |
416 | 0 | types.push(parse_type(state)?); |
417 | | } |
418 | 0 | } |
419 | | |
420 | 0 | Ok(types) |
421 | 0 | } |
422 | | |
423 | | /// Parse import statements in various forms |
424 | | /// |
425 | | /// Supports: |
426 | | /// - Simple imports: `import std::collections::HashMap` |
427 | | /// - Multiple imports: `import std::io::{Read, Write}` |
428 | | /// - Aliased imports: `import std::collections::{HashMap as Map}` |
429 | | /// - Wildcard imports: `import std::collections::*` |
430 | | /// |
431 | | /// # Examples |
432 | | /// |
433 | | /// ``` |
434 | | /// use ruchy::frontend::parser::Parser; |
435 | | /// use ruchy::frontend::ast::{ExprKind, ImportItem}; |
436 | | /// |
437 | | /// let mut parser = Parser::new("import std::collections::HashMap"); |
438 | | /// let expr = parser.parse().unwrap(); |
439 | | /// |
440 | | /// match &expr.kind { |
441 | | /// ExprKind::Import { path, items } => { |
442 | | /// assert_eq!(path, "std::collections::HashMap"); |
443 | | /// assert_eq!(items.len(), 1); |
444 | | /// assert!(matches!(items[0], ImportItem::Named(ref name) if name == "HashMap")); |
445 | | /// } |
446 | | /// _ => panic!("Expected Import expression"), |
447 | | /// } |
448 | | /// ``` |
449 | | /// |
450 | | /// ``` |
451 | | /// use ruchy::frontend::parser::Parser; |
452 | | /// use ruchy::frontend::ast::{ExprKind, ImportItem}; |
453 | | /// |
454 | | /// // Multiple imports with alias |
455 | | /// let mut parser = Parser::new("import std::collections::{HashMap as Map, Vec}"); |
456 | | /// let expr = parser.parse().unwrap(); |
457 | | /// |
458 | | /// match &expr.kind { |
459 | | /// ExprKind::Import { path, items } => { |
460 | | /// assert_eq!(path, "std::collections"); |
461 | | /// assert_eq!(items.len(), 2); |
462 | | /// assert!(matches!(&items[0], ImportItem::Aliased { name, alias } |
463 | | /// if name == "HashMap" && alias == "Map")); |
464 | | /// assert!(matches!(&items[1], ImportItem::Named(name) if name == "Vec")); |
465 | | /// } |
466 | | /// _ => panic!("Expected Import expression"), |
467 | | /// } |
468 | | /// ``` |
469 | | /// |
470 | | /// # Errors |
471 | | /// |
472 | | /// Returns an error if: |
473 | | /// - No identifier follows the import keyword |
474 | | /// - Invalid syntax in import specification |
475 | | /// - Unexpected tokens in import list |
476 | | /// |
477 | | /// Parse import statement (complexity: 7) |
478 | | /// Orchestrates URL and regular import parsing |
479 | 0 | pub fn parse_import(state: &mut ParserState) -> Result<Expr> { |
480 | 0 | let start_span = state.tokens.advance().expect("checked by parser logic").1; |
481 | | |
482 | | // Check for URL import first |
483 | 0 | if let Some((Token::String(url), _)) = state.tokens.peek() { |
484 | 0 | let url = url.clone(); |
485 | 0 | return parse_url_import(state, &url, start_span); |
486 | 0 | } |
487 | | |
488 | | // Parse regular module import |
489 | 0 | let path_parts = parse_module_path(state)?; |
490 | 0 | let items = parse_import_items(state, &path_parts)?; |
491 | | |
492 | 0 | create_import_expression(path_parts, items, start_span) |
493 | 0 | } |
494 | | |
495 | | /// Parse URL import statement (complexity: 6) |
496 | 0 | fn parse_url_import(state: &mut ParserState, url: &str, start_span: Span) -> Result<Expr> { |
497 | | // Validate URL format |
498 | 0 | if !url.starts_with("https://") && !url.starts_with("http://") { |
499 | 0 | bail!("URL imports must start with 'https://' or 'http://'"); |
500 | 0 | } |
501 | | |
502 | | // Safety validation for URL imports |
503 | 0 | validate_url_import(url)?; |
504 | | |
505 | 0 | state.tokens.advance(); |
506 | | |
507 | 0 | Ok(Expr::new( |
508 | 0 | ExprKind::Import { |
509 | 0 | path: url.to_string(), |
510 | 0 | items: vec![ImportItem::Named("*".to_string())], |
511 | 0 | }, |
512 | 0 | start_span, |
513 | 0 | )) |
514 | 0 | } |
515 | | |
516 | | /// Parse module path components (complexity: 8) |
517 | 0 | fn parse_module_path(state: &mut ParserState) -> Result<Vec<String>> { |
518 | 0 | let mut path_parts = Vec::new(); |
519 | | |
520 | | // Get first identifier |
521 | 0 | if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
522 | 0 | path_parts.push(name.clone()); |
523 | 0 | state.tokens.advance(); |
524 | | |
525 | | // Parse additional path segments |
526 | 0 | while matches!(state.tokens.peek(), Some((Token::ColonColon, _))) { |
527 | 0 | state.tokens.advance(); // consume :: |
528 | | |
529 | | // Check if this is the start of import items |
530 | 0 | if is_import_items_start(state) { |
531 | 0 | break; |
532 | 0 | } |
533 | | |
534 | 0 | path_parts.push(parse_path_segment(state)?); |
535 | | } |
536 | 0 | } |
537 | | |
538 | 0 | Ok(path_parts) |
539 | 0 | } |
540 | | |
541 | | /// Check if current position is start of import items (complexity: 2) |
542 | 0 | fn is_import_items_start(state: &mut ParserState) -> bool { |
543 | 0 | matches!( |
544 | 0 | state.tokens.peek(), |
545 | | Some((Token::Star | Token::LeftBrace, _)) |
546 | | ) |
547 | 0 | } |
548 | | |
549 | | /// Parse single path segment after :: (complexity: 3) |
550 | 0 | fn parse_path_segment(state: &mut ParserState) -> Result<String> { |
551 | 0 | if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
552 | 0 | let name = name.clone(); |
553 | 0 | state.tokens.advance(); |
554 | 0 | Ok(name) |
555 | | } else { |
556 | 0 | bail!("Expected identifier, '*', or '{{' after '::'"); |
557 | | } |
558 | 0 | } |
559 | | |
560 | | /// Parse import items (wildcard, braced list, or simple) (complexity: 9) |
561 | 0 | fn parse_import_items(state: &mut ParserState, path_parts: &[String]) -> Result<Vec<ImportItem>> { |
562 | 0 | if matches!(state.tokens.peek(), Some((Token::Star, _))) { |
563 | 0 | parse_wildcard_import(state) |
564 | 0 | } else if matches!(state.tokens.peek(), Some((Token::LeftBrace, _))) { |
565 | 0 | parse_braced_import_list(state) |
566 | | } else { |
567 | 0 | parse_simple_import(state, path_parts) |
568 | | } |
569 | 0 | } |
570 | | |
571 | | /// Parse wildcard import (* syntax) (complexity: 2) |
572 | 0 | fn parse_wildcard_import(state: &mut ParserState) -> Result<Vec<ImportItem>> { |
573 | 0 | state.tokens.advance(); // consume * |
574 | 0 | Ok(vec![ImportItem::Wildcard]) |
575 | 0 | } |
576 | | |
577 | | /// Parse braced import list ({item1, item2, ...}) (complexity: 10) |
578 | 0 | fn parse_braced_import_list(state: &mut ParserState) -> Result<Vec<ImportItem>> { |
579 | 0 | state.tokens.expect(&Token::LeftBrace)?; |
580 | | |
581 | 0 | let mut items = Vec::new(); |
582 | 0 | while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { |
583 | 0 | if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
584 | 0 | let name = name.clone(); |
585 | 0 | state.tokens.advance(); |
586 | | |
587 | 0 | let item = parse_import_item_with_alias(state, name)?; |
588 | 0 | items.push(item); |
589 | | |
590 | 0 | if !handle_item_separator(state)? { |
591 | 0 | break; |
592 | 0 | } |
593 | | } else { |
594 | 0 | validate_braced_list_token(state)?; |
595 | | } |
596 | | } |
597 | | |
598 | 0 | state.tokens.expect(&Token::RightBrace)?; |
599 | 0 | Ok(items) |
600 | 0 | } |
601 | | |
602 | | /// Parse import item with optional alias (complexity: 6) |
603 | 0 | fn parse_import_item_with_alias(state: &mut ParserState, name: String) -> Result<ImportItem> { |
604 | 0 | if matches!(state.tokens.peek(), Some((Token::As, _))) { |
605 | 0 | state.tokens.advance(); // consume as |
606 | 0 | if let Some((Token::Identifier(alias), _)) = state.tokens.peek() { |
607 | 0 | let alias = alias.clone(); |
608 | 0 | state.tokens.advance(); |
609 | 0 | Ok(ImportItem::Aliased { name, alias }) |
610 | | } else { |
611 | 0 | bail!("Expected alias name after 'as'"); |
612 | | } |
613 | | } else { |
614 | 0 | Ok(ImportItem::Named(name)) |
615 | | } |
616 | 0 | } |
617 | | |
618 | | /// Handle item separator in braced list (complexity: 4) |
619 | 0 | fn handle_item_separator(state: &mut ParserState) -> Result<bool> { |
620 | 0 | if matches!(state.tokens.peek(), Some((Token::Comma, _))) { |
621 | 0 | state.tokens.advance(); |
622 | 0 | Ok(true) // Continue parsing |
623 | 0 | } else if matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { |
624 | 0 | Ok(false) // End of list |
625 | | } else { |
626 | 0 | bail!("Expected ',' or '}}' in import list"); |
627 | | } |
628 | 0 | } |
629 | | |
630 | | /// Validate token in braced import list (complexity: 3) |
631 | 0 | fn validate_braced_list_token(state: &mut ParserState) -> Result<()> { |
632 | 0 | if !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { |
633 | 0 | bail!("Expected identifier or '}}' in import list"); |
634 | 0 | } |
635 | 0 | Ok(()) |
636 | 0 | } |
637 | | |
638 | | /// Parse simple import (path or path as alias) (complexity: 8) |
639 | 0 | fn parse_simple_import(state: &mut ParserState, path_parts: &[String]) -> Result<Vec<ImportItem>> { |
640 | 0 | if matches!(state.tokens.peek(), Some((Token::As, _))) { |
641 | 0 | parse_simple_import_with_alias(state, path_parts) |
642 | | } else { |
643 | 0 | parse_simple_import_without_alias(path_parts) |
644 | | } |
645 | 0 | } |
646 | | |
647 | | /// Parse simple import with alias (complexity: 5) |
648 | 0 | fn parse_simple_import_with_alias(state: &mut ParserState, path_parts: &[String]) -> Result<Vec<ImportItem>> { |
649 | 0 | state.tokens.advance(); // consume as |
650 | 0 | if let Some((Token::Identifier(alias), _)) = state.tokens.peek() { |
651 | 0 | let alias = alias.clone(); |
652 | 0 | state.tokens.advance(); |
653 | 0 | Ok(vec![ImportItem::Aliased { |
654 | 0 | name: path_parts.last().unwrap_or(&String::new()).clone(), |
655 | 0 | alias, |
656 | 0 | }]) |
657 | | } else { |
658 | 0 | bail!("Expected alias name after 'as'"); |
659 | | } |
660 | 0 | } |
661 | | |
662 | | /// Parse simple import without alias (complexity: 5) |
663 | 0 | fn parse_simple_import_without_alias(path_parts: &[String]) -> Result<Vec<ImportItem>> { |
664 | 0 | if path_parts.is_empty() { |
665 | 0 | Ok(Vec::new()) |
666 | 0 | } else if path_parts.len() == 1 { |
667 | | // Single segment - treat as wildcard |
668 | 0 | Ok(Vec::new()) |
669 | | } else { |
670 | | // Multi-segment - import the last part |
671 | 0 | Ok(vec![ImportItem::Named( |
672 | 0 | path_parts |
673 | 0 | .last() |
674 | 0 | .expect("checked: !path_parts.is_empty()") |
675 | 0 | .clone(), |
676 | 0 | )]) |
677 | | } |
678 | 0 | } |
679 | | |
680 | | /// Create final import expression (complexity: 4) |
681 | 0 | fn create_import_expression(path_parts: Vec<String>, items: Vec<ImportItem>, start_span: Span) -> Result<Expr> { |
682 | 0 | let path = path_parts.join("::"); |
683 | | |
684 | | // Validate that we have either a path or items |
685 | 0 | if path.is_empty() && items.is_empty() { |
686 | 0 | bail!("Expected import path or items after 'import'"); |
687 | 0 | } |
688 | | |
689 | 0 | Ok(Expr::new(ExprKind::Import { path, items }, start_span)) |
690 | 0 | } |
691 | | |
692 | | /// # Errors |
693 | | /// |
694 | | /// Returns an error if the operation fails |
695 | | /// # Errors |
696 | | /// |
697 | | /// Returns an error if the operation fails |
698 | 0 | pub fn parse_attributes(state: &mut ParserState) -> Result<Vec<Attribute>> { |
699 | 0 | let mut attributes = Vec::new(); |
700 | | |
701 | 0 | while matches!(state.tokens.peek(), Some((Token::Hash, _))) { |
702 | 0 | state.tokens.advance(); // consume # |
703 | | |
704 | 0 | if !matches!(state.tokens.peek(), Some((Token::LeftBracket, _))) { |
705 | 0 | bail!("Expected '[' after '#'"); |
706 | 0 | } |
707 | 0 | state.tokens.advance(); // consume [ |
708 | | |
709 | 0 | let name = if let Some((Token::Identifier(n), _)) = state.tokens.peek() { |
710 | 0 | let name = n.clone(); |
711 | 0 | state.tokens.advance(); |
712 | 0 | name |
713 | | } else { |
714 | 0 | bail!("Expected attribute name"); |
715 | | }; |
716 | | |
717 | 0 | let mut args = Vec::new(); |
718 | 0 | if matches!(state.tokens.peek(), Some((Token::LeftParen, _))) { |
719 | 0 | state.tokens.advance(); // consume ( |
720 | | |
721 | 0 | while !matches!(state.tokens.peek(), Some((Token::RightParen, _))) { |
722 | 0 | if let Some((Token::Identifier(arg), _)) = state.tokens.peek() { |
723 | 0 | args.push(arg.clone()); |
724 | 0 | state.tokens.advance(); |
725 | | |
726 | 0 | if matches!(state.tokens.peek(), Some((Token::Comma, _))) { |
727 | 0 | state.tokens.advance(); |
728 | 0 | } else { |
729 | 0 | break; |
730 | | } |
731 | | } else { |
732 | 0 | break; |
733 | | } |
734 | | } |
735 | | |
736 | 0 | state.tokens.advance(); // consume ) |
737 | 0 | } |
738 | | |
739 | 0 | let end_span = state.tokens.advance().expect("Expected ']' token").1; // consume ] |
740 | | |
741 | 0 | attributes.push(Attribute { |
742 | 0 | name, |
743 | 0 | args, |
744 | 0 | span: end_span, |
745 | 0 | }); |
746 | | } |
747 | | |
748 | 0 | Ok(attributes) |
749 | 0 | } |
750 | | |
751 | | /// Parse string interpolation from a string containing {expr} patterns |
752 | 0 | pub fn parse_string_interpolation(_state: &mut ParserState, s: &str) -> Vec<StringPart> { |
753 | 0 | let mut parts = Vec::new(); |
754 | 0 | let mut chars = s.chars().peekable(); |
755 | 0 | let mut current_text = String::new(); |
756 | | |
757 | 0 | while let Some(ch) = chars.next() { |
758 | 0 | match ch { |
759 | 0 | '{' if chars.peek() == Some(&'{') => { |
760 | 0 | handle_escaped_brace(&mut chars, &mut current_text, '{'); |
761 | 0 | } |
762 | 0 | '}' if chars.peek() == Some(&'}') => { |
763 | 0 | handle_escaped_brace(&mut chars, &mut current_text, '}'); |
764 | 0 | } |
765 | 0 | '{' => { |
766 | 0 | handle_interpolation(&mut chars, &mut parts, &mut current_text); |
767 | 0 | } |
768 | 0 | _ => current_text.push(ch), |
769 | | } |
770 | | } |
771 | | |
772 | 0 | finalize_text_part(&mut parts, current_text); |
773 | 0 | parts |
774 | 0 | } |
775 | | |
776 | | // Helper: Handle escaped braces (complexity: 2) |
777 | 0 | fn handle_escaped_brace<T: Iterator<Item = char>>( |
778 | 0 | chars: &mut T, |
779 | 0 | current_text: &mut String, |
780 | 0 | brace_char: char, |
781 | 0 | ) { |
782 | 0 | chars.next(); // consume second brace |
783 | 0 | current_text.push(brace_char); |
784 | 0 | } |
785 | | |
786 | | // Helper: Handle interpolation expressions (complexity: 4) |
787 | 0 | fn handle_interpolation<T: Iterator<Item = char>>( |
788 | 0 | chars: &mut T, |
789 | 0 | parts: &mut Vec<StringPart>, |
790 | 0 | current_text: &mut String, |
791 | 0 | ) { |
792 | 0 | if !current_text.is_empty() { |
793 | 0 | parts.push(StringPart::Text(current_text.clone())); |
794 | 0 | current_text.clear(); |
795 | 0 | } |
796 | | |
797 | 0 | let expr_text = extract_expression_text(chars); |
798 | 0 | let string_part = parse_interpolated_expr(&expr_text); |
799 | 0 | parts.push(string_part); |
800 | 0 | } |
801 | | |
802 | | // Helper: Extract expression text from braces (complexity: 8) |
803 | 0 | fn extract_expression_text<T: Iterator<Item = char>>(chars: &mut T) -> String { |
804 | 0 | let mut expr_text = String::new(); |
805 | 0 | let mut context = ExprContext::default(); |
806 | | |
807 | 0 | for expr_ch in chars { |
808 | 0 | if process_character(expr_ch, &mut context, &mut expr_text) { |
809 | 0 | break; |
810 | 0 | } |
811 | | } |
812 | | |
813 | 0 | expr_text |
814 | 0 | } |
815 | | |
816 | | /// Process a single character in expression extraction (complexity: 8) |
817 | 0 | fn process_character(ch: char, context: &mut ExprContext, expr_text: &mut String) -> bool { |
818 | 0 | match ch { |
819 | 0 | '"' if should_process_string_quote(context) => { |
820 | 0 | handle_string_delimiter(context); |
821 | 0 | expr_text.push(ch); |
822 | 0 | } |
823 | 0 | '\'' if should_process_char_quote(context) => { |
824 | 0 | handle_char_delimiter(context); |
825 | 0 | expr_text.push(ch); |
826 | 0 | } |
827 | 0 | '{' if should_process_brace(context) => { |
828 | 0 | handle_open_brace(context); |
829 | 0 | expr_text.push(ch); |
830 | 0 | } |
831 | 0 | '}' if should_process_brace(context) => { |
832 | 0 | handle_close_brace(context); |
833 | 0 | if should_terminate(context) { |
834 | 0 | return true; // Signal to break the loop |
835 | 0 | } |
836 | 0 | expr_text.push(ch); |
837 | | } |
838 | 0 | '\\' if should_escape(context) => { |
839 | 0 | handle_escape(context); |
840 | 0 | expr_text.push(ch); |
841 | 0 | } |
842 | 0 | _ => { |
843 | 0 | handle_regular_char(context, ch); |
844 | 0 | expr_text.push(ch); |
845 | 0 | } |
846 | | } |
847 | | |
848 | | // Reset escape flag for non-backslash characters |
849 | 0 | reset_escape_flag(context, ch); |
850 | 0 | false // Continue processing |
851 | 0 | } |
852 | | |
853 | | /// Check if string quote should be processed (complexity: 1) |
854 | 0 | fn should_process_string_quote(context: &ExprContext) -> bool { |
855 | 0 | !context.in_char && !context.escaped |
856 | 0 | } |
857 | | |
858 | | /// Check if char quote should be processed (complexity: 1) |
859 | 0 | fn should_process_char_quote(context: &ExprContext) -> bool { |
860 | 0 | !context.in_string && !context.escaped |
861 | 0 | } |
862 | | |
863 | | /// Check if brace should be processed (complexity: 1) |
864 | 0 | fn should_process_brace(context: &ExprContext) -> bool { |
865 | 0 | !context.in_string && !context.in_char |
866 | 0 | } |
867 | | |
868 | | /// Check if escape should be handled (complexity: 1) |
869 | 0 | fn should_escape(context: &ExprContext) -> bool { |
870 | 0 | (context.in_string || context.in_char) && !context.escaped |
871 | 0 | } |
872 | | |
873 | | /// Toggle string delimiter state (complexity: 1) |
874 | 0 | fn handle_string_delimiter(context: &mut ExprContext) { |
875 | 0 | context.in_string = !context.in_string; |
876 | 0 | } |
877 | | |
878 | | /// Toggle char delimiter state (complexity: 1) |
879 | 0 | fn handle_char_delimiter(context: &mut ExprContext) { |
880 | 0 | context.in_char = !context.in_char; |
881 | 0 | } |
882 | | |
883 | | /// Increment brace count (complexity: 1) |
884 | 0 | fn handle_open_brace(context: &mut ExprContext) { |
885 | 0 | context.brace_count += 1; |
886 | 0 | } |
887 | | |
888 | | /// Decrement brace count (complexity: 1) |
889 | 0 | fn handle_close_brace(context: &mut ExprContext) { |
890 | 0 | context.brace_count -= 1; |
891 | 0 | } |
892 | | |
893 | | /// Set escape flag (complexity: 1) |
894 | 0 | fn handle_escape(context: &mut ExprContext) { |
895 | 0 | context.escaped = true; |
896 | 0 | } |
897 | | |
898 | | /// Handle regular character (complexity: 1) |
899 | 0 | fn handle_regular_char(context: &mut ExprContext, _ch: char) { |
900 | 0 | context.escaped = false; |
901 | 0 | } |
902 | | |
903 | | /// Reset escape flag if needed (complexity: 2) |
904 | 0 | fn reset_escape_flag(context: &mut ExprContext, ch: char) { |
905 | 0 | if ch != '\\' { |
906 | 0 | context.escaped = false; |
907 | 0 | } |
908 | 0 | } |
909 | | |
910 | | /// Check if we should terminate extraction (complexity: 1) |
911 | 0 | fn should_terminate(context: &ExprContext) -> bool { |
912 | 0 | context.brace_count == 0 |
913 | 0 | } |
914 | | |
915 | | // Helper: Parse interpolated expression with format specifier (complexity: 4) |
916 | 0 | fn parse_interpolated_expr(expr_text: &str) -> StringPart { |
917 | 0 | let (expr_part, format_spec) = split_format_specifier(expr_text); |
918 | | |
919 | 0 | let mut expr_parser = super::core::Parser::new(expr_part); |
920 | 0 | match expr_parser.parse() { |
921 | 0 | Ok(expr) => { |
922 | 0 | if let Some(spec) = format_spec { |
923 | 0 | StringPart::ExprWithFormat { |
924 | 0 | expr: Box::new(expr), |
925 | 0 | format_spec: spec.to_string(), |
926 | 0 | } |
927 | | } else { |
928 | 0 | StringPart::Expr(Box::new(expr)) |
929 | | } |
930 | | } |
931 | | Err(_) => { |
932 | | // Fallback to text if parsing fails |
933 | 0 | StringPart::Text(format!("{{{expr_text}}}")) |
934 | | } |
935 | | } |
936 | 0 | } |
937 | | |
938 | | // Helper: Split format specifier from expression (complexity: 3) |
939 | 0 | fn split_format_specifier(expr_text: &str) -> (&str, Option<&str>) { |
940 | 0 | if let Some(colon_pos) = expr_text.find(':') { |
941 | 0 | let before_colon = &expr_text[..colon_pos]; |
942 | 0 | if !before_colon.contains('"') && !before_colon.contains('\'') { |
943 | 0 | (before_colon, Some(&expr_text[colon_pos..])) |
944 | | } else { |
945 | 0 | (expr_text, None) |
946 | | } |
947 | | } else { |
948 | 0 | (expr_text, None) |
949 | | } |
950 | 0 | } |
951 | | |
952 | | // Helper: Finalize remaining text (complexity: 2) |
953 | 0 | fn finalize_text_part(parts: &mut Vec<StringPart>, current_text: String) { |
954 | 0 | if !current_text.is_empty() { |
955 | 0 | parts.push(StringPart::Text(current_text)); |
956 | 0 | } |
957 | 0 | } |
958 | | |
959 | | // Helper struct to track expression parsing context (complexity: 0) |
960 | | #[derive(Default)] |
961 | | struct ExprContext { |
962 | | brace_count: i32, |
963 | | in_string: bool, |
964 | | in_char: bool, |
965 | | escaped: bool, |
966 | | } |
967 | | |
968 | | impl ExprContext { |
969 | 0 | fn default() -> Self { |
970 | 0 | Self { |
971 | 0 | brace_count: 1, |
972 | 0 | in_string: false, |
973 | 0 | in_char: false, |
974 | 0 | escaped: false, |
975 | 0 | } |
976 | 0 | } |
977 | | } |
978 | | |
979 | | /// Parse module declarations |
980 | | /// |
981 | | /// Supports: |
982 | | /// - Empty modules: `module MyModule {}` |
983 | | /// - Single expression modules: `module Math { sqrt(x) }` |
984 | | /// - Multi-expression modules: `module Utils { fn helper() {...}; const PI = 3.14 }` |
985 | | /// |
986 | | /// # Examples |
987 | | /// |
988 | | /// ``` |
989 | | /// use ruchy::frontend::parser::Parser; |
990 | | /// use ruchy::frontend::ast::ExprKind; |
991 | | /// |
992 | | /// // Empty module |
993 | | /// let mut parser = Parser::new("module Empty {}"); |
994 | | /// let expr = parser.parse().unwrap(); |
995 | | /// |
996 | | /// match &expr.kind { |
997 | | /// ExprKind::Module { name, .. } => { |
998 | | /// assert_eq!(name, "Empty"); |
999 | | /// } |
1000 | | /// _ => panic!("Expected Module expression"), |
1001 | | /// } |
1002 | | /// ``` |
1003 | | /// |
1004 | | /// ``` |
1005 | | /// use ruchy::frontend::parser::Parser; |
1006 | | /// use ruchy::frontend::ast::{ExprKind, Literal}; |
1007 | | /// |
1008 | | /// // Module with content |
1009 | | /// let mut parser = Parser::new("module Math { 42 }"); |
1010 | | /// let expr = parser.parse().unwrap(); |
1011 | | /// |
1012 | | /// match &expr.kind { |
1013 | | /// ExprKind::Module { name, body } => { |
1014 | | /// assert_eq!(name, "Math"); |
1015 | | /// // Verify body contains literal 42 |
1016 | | /// match &body.kind { |
1017 | | /// ExprKind::Literal(Literal::Integer(n)) => assert_eq!(*n, 42), |
1018 | | /// _ => panic!("Expected integer literal in module body"), |
1019 | | /// } |
1020 | | /// } |
1021 | | /// _ => panic!("Expected Module expression"), |
1022 | | /// } |
1023 | | /// ``` |
1024 | | /// |
1025 | | /// # Errors |
1026 | | /// |
1027 | | /// Returns an error if: |
1028 | | /// - No identifier follows the module keyword |
1029 | | /// - Missing opening or closing braces |
1030 | | /// - Invalid syntax in module body |
1031 | 0 | pub fn parse_module(state: &mut ParserState) -> Result<Expr> { |
1032 | 0 | let start_span = state.tokens.advance().expect("checked by parser logic").1; // consume module |
1033 | | |
1034 | | // Parse module name |
1035 | 0 | let name = if let Some((Token::Identifier(n), _)) = state.tokens.peek() { |
1036 | 0 | let name = n.clone(); |
1037 | 0 | state.tokens.advance(); |
1038 | 0 | name |
1039 | | } else { |
1040 | 0 | bail!("Expected module name after 'module'"); |
1041 | | }; |
1042 | | |
1043 | | // Expect opening brace |
1044 | 0 | state.tokens.expect(&Token::LeftBrace)?; |
1045 | | |
1046 | | // Parse module body (can be a block or single expression) |
1047 | 0 | let body = if matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { |
1048 | | // Empty module |
1049 | 0 | Box::new(Expr::new( |
1050 | 0 | ExprKind::Literal(Literal::Unit), |
1051 | 0 | Span { start: 0, end: 0 }, |
1052 | | )) |
1053 | | } else { |
1054 | | // Parse expressions until we hit the closing brace |
1055 | 0 | let mut exprs = Vec::new(); |
1056 | 0 | while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { |
1057 | 0 | exprs.push(super::parse_expr_recursive(state)?); |
1058 | | // Optional semicolon or comma separator |
1059 | 0 | if matches!( |
1060 | 0 | state.tokens.peek(), |
1061 | | Some((Token::Semicolon | Token::Comma, _)) |
1062 | 0 | ) { |
1063 | 0 | state.tokens.advance(); |
1064 | 0 | } |
1065 | | } |
1066 | | |
1067 | 0 | if exprs.len() == 1 { |
1068 | 0 | Box::new(exprs.into_iter().next().expect("checked: exprs.len() == 1")) |
1069 | | } else { |
1070 | 0 | Box::new(Expr::new(ExprKind::Block(exprs), Span { start: 0, end: 0 })) |
1071 | | } |
1072 | | }; |
1073 | | |
1074 | | // Expect closing brace |
1075 | 0 | state.tokens.expect(&Token::RightBrace)?; |
1076 | | |
1077 | 0 | Ok(Expr::new(ExprKind::Module { name, body }, start_span)) |
1078 | 0 | } |
1079 | | |
1080 | | /// Parse export statements |
1081 | | /// |
1082 | | /// Supports: |
1083 | | /// - Single exports: `export myFunction` |
1084 | | /// - Multiple exports: `export { func1, func2, func3 }` |
1085 | | /// |
1086 | | /// # Examples |
1087 | | /// |
1088 | | /// ``` |
1089 | | /// use ruchy::frontend::parser::Parser; |
1090 | | /// use ruchy::frontend::ast::ExprKind; |
1091 | | /// |
1092 | | /// // Single export |
1093 | | /// let mut parser = Parser::new("export myFunction"); |
1094 | | /// let expr = parser.parse().unwrap(); |
1095 | | /// |
1096 | | /// match &expr.kind { |
1097 | | /// ExprKind::Export { items } => { |
1098 | | /// assert_eq!(items.len(), 1); |
1099 | | /// assert_eq!(items[0], "myFunction"); |
1100 | | /// } |
1101 | | /// _ => panic!("Expected Export expression"), |
1102 | | /// } |
1103 | | /// ``` |
1104 | | /// |
1105 | | /// ``` |
1106 | | /// use ruchy::frontend::parser::Parser; |
1107 | | /// use ruchy::frontend::ast::ExprKind; |
1108 | | /// |
1109 | | /// // Multiple exports |
1110 | | /// let mut parser = Parser::new("export { add, subtract, multiply }"); |
1111 | | /// let expr = parser.parse().unwrap(); |
1112 | | /// |
1113 | | /// match &expr.kind { |
1114 | | /// ExprKind::Export { items } => { |
1115 | | /// assert_eq!(items.len(), 3); |
1116 | | /// assert!(items.contains(&"add".to_string())); |
1117 | | /// assert!(items.contains(&"subtract".to_string())); |
1118 | | /// assert!(items.contains(&"multiply".to_string())); |
1119 | | /// } |
1120 | | /// _ => panic!("Expected Export expression"), |
1121 | | /// } |
1122 | | /// ``` |
1123 | | /// |
1124 | | /// # Errors |
1125 | | /// |
1126 | | /// Returns an error if: |
1127 | | /// - No identifier or brace follows the export keyword |
1128 | | /// - Invalid syntax in export list |
1129 | | /// - Missing closing brace in export block |
1130 | 0 | pub fn parse_export(state: &mut ParserState) -> Result<Expr> { |
1131 | 0 | let start_span = state.tokens.advance().expect("checked by parser logic").1; // consume export |
1132 | | |
1133 | 0 | let mut items = Vec::new(); |
1134 | | |
1135 | | // Parse export list |
1136 | 0 | if matches!(state.tokens.peek(), Some((Token::LeftBrace, _))) { |
1137 | | // Export block: export { item1, item2, ... } |
1138 | 0 | state.tokens.advance(); // consume { |
1139 | | |
1140 | 0 | while !matches!(state.tokens.peek(), Some((Token::RightBrace, _))) { |
1141 | 0 | if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
1142 | 0 | items.push(name.clone()); |
1143 | 0 | state.tokens.advance(); |
1144 | | |
1145 | 0 | if matches!(state.tokens.peek(), Some((Token::Comma, _))) { |
1146 | 0 | state.tokens.advance(); // consume comma |
1147 | 0 | } else { |
1148 | 0 | break; |
1149 | | } |
1150 | | } else { |
1151 | 0 | bail!("Expected identifier in export list"); |
1152 | | } |
1153 | | } |
1154 | | |
1155 | 0 | state.tokens.expect(&Token::RightBrace)?; |
1156 | 0 | } else if let Some((Token::Identifier(name), _)) = state.tokens.peek() { |
1157 | 0 | // Single export: export item |
1158 | 0 | items.push(name.clone()); |
1159 | 0 | state.tokens.advance(); |
1160 | 0 | } else { |
1161 | 0 | bail!("Expected export list or identifier after 'export'"); |
1162 | | } |
1163 | | |
1164 | 0 | Ok(Expr::new(ExprKind::Export { items }, start_span)) |
1165 | 0 | } |