/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 | } |