polydat_grammar/comprehension/spec/
source_parser.rs1use crate::comprehension::cardinality::{Interval, ProductMeasure};
34use crate::comprehension::source::{LiteralValue, Source};
35
36pub fn parse_source(text: &str) -> Result<Source, SourceParseError> {
38 let trimmed = text.trim();
39
40 if let Some(name) = strip_curly(trimmed) {
46 return Ok(Source::WorkloadParamList {
47 name,
48 len_hint: None,
49 });
50 }
51 if let Some(dyn_text) = strip_dynamic_curly(trimmed) {
52 return Ok(Source::WorkloadParamList {
53 name: dyn_text,
54 len_hint: None,
55 });
56 }
57
58 if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') {
67 let inner = &trimmed[1..trimmed.len() - 1];
68 let values = super::super::source::split_string_comprehension(inner)
69 .into_iter()
70 .map(parse_literal_value)
71 .collect();
72 return Ok(Source::Literal { values });
73 }
74 if trimmed.len() >= 2 && trimmed.starts_with('\'') && trimmed.ends_with('\'') {
75 let inner = &trimmed[1..trimmed.len() - 1];
76 return Ok(Source::Literal {
77 values: vec![LiteralValue::String(inner.to_string())],
78 });
79 }
80
81 if trimmed.starts_with('[') && trimmed.ends_with(']') {
92 let inner = &trimmed[1..trimmed.len() - 1];
93 if bracket_is_pure_literal(inner) {
94 return parse_literal_list(inner);
95 }
96 return Ok(Source::Generator {
97 expr: trimmed.to_string(),
98 cardinality_hint: None,
99 });
100 }
101
102 if let Some(idx) = find_top_level(trimmed, "..") {
104 return parse_range(trimmed, idx);
105 }
106
107 if looks_like_function_call(trimmed) {
109 return Ok(Source::Generator {
110 expr: trimmed.to_string(),
111 cardinality_hint: None,
112 });
113 }
114
115 if let Some(value) = try_parse_bare_scalar(trimmed) {
121 return Ok(Source::Literal {
122 values: vec![value],
123 });
124 }
125
126 if trimmed.contains(',') && looks_like_bare_value_list(trimmed) {
133 return parse_literal_list(trimmed);
134 }
135
136 Ok(Source::Generator {
144 expr: trimmed.to_string(),
145 cardinality_hint: None,
146 })
147}
148
149fn looks_like_bare_value_list(text: &str) -> bool {
155 !text.chars().any(|c| {
156 matches!(
157 c,
158 '(' | ')'
159 | '['
160 | ']'
161 | '{'
162 | '}'
163 | '\''
164 | '"'
165 | '+'
166 | '*'
167 | '/'
168 | '%'
169 | '='
170 | '<'
171 | '>'
172 | '!'
173 | '&'
174 | '|'
175 | '~'
176 | '^'
177 | '?'
178 )
179 })
180}
181
182fn strip_dynamic_curly(s: &str) -> Option<String> {
186 let s = s.trim();
187 if !s.starts_with('{') || !s.ends_with('}') {
188 return None;
189 }
190 let inner = &s[1..s.len() - 1];
191 if !inner.contains('{') {
195 return None;
196 }
197 Some(inner.to_string())
198}
199
200fn parse_literal_list(inner: &str) -> Result<Source, SourceParseError> {
204 let parts: Vec<&str> = inner
205 .split(',')
206 .map(|s| s.trim())
207 .filter(|s| !s.is_empty())
208 .collect();
209
210 if parts.is_empty() {
211 return Ok(Source::Literal { values: Vec::new() });
212 }
213
214 let values: Vec<LiteralValue> = parts.iter().map(|s| parse_literal_value(s)).collect();
215
216 Ok(Source::Literal { values })
217}
218
219fn bracket_is_pure_literal(inner: &str) -> bool {
226 let elems: Vec<&str> = inner
227 .split(',')
228 .map(str::trim)
229 .filter(|s| !s.is_empty())
230 .collect();
231 if elems.is_empty() {
232 return true; }
234 elems.iter().all(|e| {
235 if e.ends_with('…') || e.ends_with("...") {
236 return false; }
238 e.eq_ignore_ascii_case("true")
239 || e.eq_ignore_ascii_case("false")
240 || ((e.starts_with('"') && e.ends_with('"'))
241 || (e.starts_with('\'') && e.ends_with('\'')))
242 || e.parse::<i64>().is_ok()
243 || e.parse::<f64>().is_ok()
244 })
245}
246
247fn parse_literal_value(s: &str) -> LiteralValue {
248 let s = s.trim();
249 if s.eq_ignore_ascii_case("true") {
250 return LiteralValue::Bool(true);
251 }
252 if s.eq_ignore_ascii_case("false") {
253 return LiteralValue::Bool(false);
254 }
255 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
257 let inner = &s[1..s.len() - 1];
258 return LiteralValue::String(inner.to_string());
259 }
260 if let Ok(n) = s.parse::<i64>() {
262 return LiteralValue::Int(n);
263 }
264 if let Ok(f) = s.parse::<f64>() {
266 return LiteralValue::Float(f);
267 }
268 LiteralValue::String(s.to_string())
270}
271
272fn parse_range(text: &str, dotdot_idx: usize) -> Result<Source, SourceParseError> {
275 let lo_str = text[..dotdot_idx].trim();
276 let after = &text[dotdot_idx + 2..];
277
278 let (inclusive_end, after) = if let Some(rest) = after.strip_prefix('=') {
280 (true, rest)
281 } else {
282 (false, after)
283 };
284
285 let (rhs, step) = if let Some(step_pos) = after.find(" step ") {
290 let rhs = after[..step_pos].trim();
291 let step_str = after[step_pos + 6..].trim();
292 let step: i64 = step_str
293 .parse()
294 .map_err(|_| SourceParseError::InvalidRange(text.to_string()))?;
295 (rhs, step)
296 } else if let Some(step_pos) = after.find("..") {
297 let rhs = after[..step_pos].trim();
300 let step_str = after[step_pos + 2..].trim();
301 let step: i64 = step_str
302 .parse()
303 .map_err(|_| SourceParseError::InvalidRange(text.to_string()))?;
304 (rhs, step)
305 } else {
306 (after.trim(), 1)
307 };
308
309 if let (Ok(lo_i), Ok(hi_i)) = (lo_str.parse::<i64>(), rhs.parse::<i64>()) {
311 let hi = if inclusive_end { hi_i + 1 } else { hi_i };
312 return Ok(Source::IntRange { lo: lo_i, hi, step });
313 }
314 if let (Ok(lo_f), Ok(hi_f)) = (lo_str.parse::<f64>(), rhs.parse::<f64>()) {
316 let interval = Interval {
317 lo: lo_f,
318 hi: hi_f,
319 lo_open: false,
320 hi_open: !inclusive_end,
321 };
322 return Ok(Source::ContinuousInterval {
323 interval,
324 measure: ProductMeasure::Uniform,
325 });
326 }
327
328 Err(SourceParseError::InvalidRange(text.to_string()))
329}
330
331fn strip_curly(s: &str) -> Option<String> {
332 let s = s.trim();
333 if s.starts_with('{') && s.ends_with('}') {
334 let inner = &s[1..s.len() - 1];
335 let trimmed = inner.trim();
336 if !trimmed.is_empty() && trimmed.chars().all(|c| c.is_alphanumeric() || c == '_') {
337 return Some(trimmed.to_string());
338 }
339 }
340 None
341}
342
343fn try_parse_bare_scalar(s: &str) -> Option<LiteralValue> {
349 if s.eq_ignore_ascii_case("true") {
350 return Some(LiteralValue::Bool(true));
351 }
352 if s.eq_ignore_ascii_case("false") {
353 return Some(LiteralValue::Bool(false));
354 }
355 if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
356 let inner = &s[1..s.len() - 1];
357 return Some(LiteralValue::String(inner.to_string()));
358 }
359 if let Ok(n) = s.parse::<i64>() {
360 return Some(LiteralValue::Int(n));
361 }
362 if let Ok(f) = s.parse::<f64>() {
363 return Some(LiteralValue::Float(f));
364 }
365 None
366}
367
368fn looks_like_function_call(s: &str) -> bool {
369 let Some(open) = s.find('(') else {
370 return false;
371 };
372 if !s.ends_with(')') {
373 return false;
374 }
375 let name = &s[..open];
376 !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_')
377}
378
379fn find_top_level(s: &str, needle: &str) -> Option<usize> {
382 let bytes = s.as_bytes();
383 let needle_bytes = needle.as_bytes();
384 let mut depth = 0i64;
385 let mut i = 0;
386 while i + needle_bytes.len() <= bytes.len() {
387 match bytes[i] {
388 b'(' | b'[' | b'{' => depth += 1,
389 b')' | b']' | b'}' => depth -= 1,
390 _ => {}
391 }
392 if depth == 0 && &bytes[i..i + needle_bytes.len()] == needle_bytes {
393 return Some(i);
394 }
395 i += 1;
396 }
397 None
398}
399
400#[derive(Debug, Clone, PartialEq)]
402pub enum SourceParseError {
403 Unrecognized(String),
405 InvalidRange(String),
408}
409
410impl std::fmt::Display for SourceParseError {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 match self {
413 SourceParseError::Unrecognized(s) => {
414 write!(f, "unrecognized source expression: {s:?}")
415 }
416 SourceParseError::InvalidRange(s) => {
417 write!(f, "invalid range expression: {s:?}")
418 }
419 }
420 }
421}
422
423impl std::error::Error for SourceParseError {}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn int_range_exclusive() {
431 let s = parse_source("1..10").unwrap();
432 assert!(matches!(
433 s,
434 Source::IntRange {
435 lo: 1,
436 hi: 10,
437 step: 1
438 }
439 ));
440 }
441
442 #[test]
443 fn double_quoted_source_is_string_comprehension_striped() {
444 let s = parse_source(r#""rerank_def, rerank_1x, rerank_2x""#).unwrap();
446 match s {
447 Source::Literal { values } => {
448 assert_eq!(
449 values,
450 vec![
451 LiteralValue::String("rerank_def".into()),
452 LiteralValue::String("rerank_1x".into()),
453 LiteralValue::String("rerank_2x".into()),
454 ]
455 );
456 }
457 other => panic!("expected striped Literal, got {other:?}"),
458 }
459 }
460
461 #[test]
462 fn single_quoted_source_is_atomic() {
463 let s = parse_source("'rerank_def, rerank_1x'").unwrap();
465 match s {
466 Source::Literal { values } => {
467 assert_eq!(
468 values,
469 vec![LiteralValue::String("rerank_def, rerank_1x".into())]
470 );
471 }
472 other => panic!("expected atomic Literal, got {other:?}"),
473 }
474 }
475
476 #[test]
477 fn int_range_inclusive() {
478 let s = parse_source("1..=10").unwrap();
479 assert!(matches!(
480 s,
481 Source::IntRange {
482 lo: 1,
483 hi: 11,
484 step: 1
485 }
486 ));
487 }
488
489 #[test]
490 fn int_range_with_step() {
491 let s = parse_source("0..100 step 10").unwrap();
492 assert!(matches!(
493 s,
494 Source::IntRange {
495 lo: 0,
496 hi: 100,
497 step: 10
498 }
499 ));
500 }
501
502 #[test]
503 fn literal_int_list() {
504 let s = parse_source("[1, 2, 3]").unwrap();
505 match s {
506 Source::Literal { values } => {
507 assert_eq!(values.len(), 3);
508 assert_eq!(values[0], LiteralValue::Int(1));
509 assert_eq!(values[2], LiteralValue::Int(3));
510 }
511 other => panic!("expected Literal, got {other:?}"),
512 }
513 }
514
515 #[test]
516 fn bracket_bare_words_are_references_not_strings() {
517 let s = parse_source("[a, b, c]").unwrap();
523 match s {
524 Source::Generator { expr, .. } => assert_eq!(expr, "[a, b, c]"),
525 other => panic!("expected deferred Generator, got {other:?}"),
526 }
527 }
528
529 #[test]
530 fn bracket_with_spread_defers_to_generator() {
531 let s = parse_source("[xs…]").unwrap();
532 assert!(
533 matches!(s, Source::Generator { .. }),
534 "spread list must defer: {s:?}"
535 );
536 }
537
538 #[test]
539 fn literal_quoted_strings() {
540 let s = parse_source(r#"["hello", "world"]"#).unwrap();
541 match s {
542 Source::Literal { values } => {
543 assert_eq!(values[0], LiteralValue::String("hello".into()));
544 assert_eq!(values[1], LiteralValue::String("world".into()));
545 }
546 other => panic!("expected Literal, got {other:?}"),
547 }
548 }
549
550 #[test]
551 fn literal_float_list() {
552 let s = parse_source("[1.5, 2.5, 3.5]").unwrap();
553 match s {
554 Source::Literal { values } => {
555 assert_eq!(values[0], LiteralValue::Float(1.5));
556 }
557 other => panic!("expected Literal, got {other:?}"),
558 }
559 }
560
561 #[test]
562 fn workload_param_ref() {
563 let s = parse_source("{profiles}").unwrap();
564 match s {
565 Source::WorkloadParamList { name, .. } => assert_eq!(name, "profiles"),
566 other => panic!("expected WorkloadParamList, got {other:?}"),
567 }
568 }
569
570 #[test]
571 fn generator_function_call() {
572 let s = parse_source("fib(8)").unwrap();
573 match s {
574 Source::Generator { expr, .. } => assert_eq!(expr, "fib(8)"),
575 other => panic!("expected Generator, got {other:?}"),
576 }
577 }
578
579 #[test]
580 fn continuous_interval_via_floats() {
581 let s = parse_source("0.0..1.0").unwrap();
582 match s {
583 Source::ContinuousInterval { interval, measure } => {
584 assert_eq!(interval.lo, 0.0);
585 assert_eq!(interval.hi, 1.0);
586 assert!(matches!(measure, ProductMeasure::Uniform));
587 }
588 other => panic!("expected ContinuousInterval, got {other:?}"),
589 }
590 }
591
592 #[test]
593 fn continuous_interval_inclusive() {
594 let s = parse_source("0.0..=1.0").unwrap();
595 match s {
596 Source::ContinuousInterval { interval, .. } => {
597 assert!(!interval.hi_open);
598 }
599 other => panic!("expected ContinuousInterval, got {other:?}"),
600 }
601 }
602
603 #[test]
604 fn unrecognized_source_falls_back_to_generator() {
605 let s = parse_source("totally nonsense").unwrap();
611 match s {
612 Source::Generator { expr, .. } => assert_eq!(expr, "totally nonsense"),
613 other => panic!("expected Generator, got {other:?}"),
614 }
615 }
616
617 #[test]
618 fn empty_literal_list() {
619 let s = parse_source("[]").unwrap();
620 match s {
621 Source::Literal { values } => assert!(values.is_empty()),
622 other => panic!("expected empty Literal, got {other:?}"),
623 }
624 }
625}