Skip to main content

tdsl_parser/
builder.rs

1use pest::iterators::{Pair, Pairs};
2
3use crate::Rule;
4use crate::ast::*;
5use crate::error::ParseError;
6
7type Result<T> = std::result::Result<T, ParseError>;
8
9/// Build an AST [`File`] from the top-level pest parse result.
10pub fn build_file(pairs: Pairs<'_, Rule>) -> Result<File> {
11    let mut statements = Vec::new();
12    // The top-level `file` rule wraps all statements
13    let file_pair = pairs.into_iter().next().unwrap();
14    for pair in file_pair.into_inner() {
15        match pair.as_rule() {
16            Rule::timeline_block => {
17                let span = pair_span(&pair);
18                statements.push(Spanned {
19                    node: Statement::Timeline(build_timeline(pair)?),
20                    span,
21                });
22            }
23            Rule::lane_decl => {
24                let span = pair_span(&pair);
25                statements.push(Spanned {
26                    node: Statement::Lane(build_lane(pair)?),
27                    span,
28                });
29            }
30            Rule::group_decl => {
31                let span = pair_span(&pair);
32                statements.push(Spanned {
33                    node: Statement::Group(build_group(pair)?),
34                    span,
35                });
36            }
37            Rule::span_decl => {
38                let span = pair_span(&pair);
39                statements.push(Spanned {
40                    node: Statement::Span(build_span(pair)?),
41                    span,
42                });
43            }
44            Rule::event_decl => {
45                let span = pair_span(&pair);
46                statements.push(Spanned {
47                    node: Statement::Event(build_event(pair)?),
48                    span,
49                });
50            }
51            Rule::event_range_decl => {
52                let span = pair_span(&pair);
53                statements.push(Spanned {
54                    node: Statement::EventRange(build_event_range(pair)?),
55                    span,
56                });
57            }
58            Rule::import_block => {
59                let span = pair_span(&pair);
60                statements.push(Spanned {
61                    node: Statement::Import(build_import(pair)?),
62                    span,
63                });
64            }
65            Rule::map_block => {
66                let span = pair_span(&pair);
67                statements.push(Spanned {
68                    node: Statement::Map(build_map(pair)?),
69                    span,
70                });
71            }
72            Rule::template_block => {
73                let span = pair_span(&pair);
74                statements.push(Spanned {
75                    node: Statement::Template(build_template(pair)?),
76                    span,
77                });
78            }
79            Rule::apply_block => {
80                let span = pair_span(&pair);
81                statements.push(Spanned {
82                    node: Statement::Apply(build_apply(pair)?),
83                    span,
84                });
85            }
86            Rule::EOI => {}
87            _ => {}
88        }
89    }
90    Ok(File { statements })
91}
92
93// ─── Timeline ───────────────────────────────────────────────
94
95fn build_timeline(pair: Pair<'_, Rule>) -> Result<TimelineBlock> {
96    let mut inner = pair.into_inner();
97    let name = extract_string_literal(&inner.next().unwrap());
98
99    let mut block = TimelineBlock {
100        name,
101        title: None,
102        unit: None,
103        range: None,
104        calendar: None,
105        color_map: Vec::new(),
106    };
107
108    for prop in inner {
109        match prop.as_rule() {
110            Rule::title_prop => {
111                block.title = Some(extract_string_literal(&prop.into_inner().next().unwrap()));
112            }
113            Rule::unit_prop => {
114                block.unit = Some(prop.into_inner().next().unwrap().as_str().to_string());
115            }
116            Rule::range_prop => {
117                let mut nums = prop.into_inner();
118                let start = parse_time_value(nums.next().unwrap())?;
119                let end = parse_time_value(nums.next().unwrap())?;
120                block.range = Some(RangeExpr { start, end });
121            }
122            Rule::calendar_prop => {
123                block.calendar = Some(prop.into_inner().next().unwrap().as_str().to_string());
124            }
125            Rule::color_map_block => {
126                for entry in prop.into_inner() {
127                    let mut ei = entry.into_inner();
128                    let tag = ei.next().unwrap().as_str().to_string();
129                    let color = extract_string_literal(&ei.next().unwrap());
130                    block.color_map.push((tag, color));
131                }
132            }
133            _ => {}
134        }
135    }
136    Ok(block)
137}
138
139// ─── Lane ───────────────────────────────────────────────────
140
141fn build_lane(pair: Pair<'_, Rule>) -> Result<LaneDecl> {
142    let mut inner = pair.into_inner();
143    let label = extract_string_literal(&inner.next().unwrap());
144
145    let mut decl = LaneDecl {
146        label,
147        alias: None,
148        kind: None,
149        order: None,
150    };
151
152    for item in inner {
153        match item.as_rule() {
154            Rule::ident => {
155                decl.alias = Some(item.as_str().to_string());
156            }
157            Rule::kind_prop => {
158                decl.kind = Some(item.into_inner().next().unwrap().as_str().to_string());
159            }
160            Rule::order_prop => {
161                decl.order = Some(parse_int(&item.into_inner().next().unwrap())?);
162            }
163            _ => {}
164        }
165    }
166    Ok(decl)
167}
168
169// ─── Group ──────────────────────────────────────────────────
170
171fn build_group(pair: Pair<'_, Rule>) -> Result<GroupDecl> {
172    let mut inner = pair.into_inner();
173    let label = extract_string_literal(&inner.next().unwrap());
174    let mut lanes = Vec::new();
175    for item in inner {
176        if item.as_rule() == Rule::lane_decl {
177            lanes.push(build_lane(item)?);
178        }
179    }
180    Ok(GroupDecl { label, lanes })
181}
182
183// ─── Span ───────────────────────────────────────────────────
184
185fn build_span(pair: Pair<'_, Rule>) -> Result<SpanDecl> {
186    let mut inner = pair.into_inner();
187    let lane_ref = inner.next().unwrap().as_str().to_string();
188    let start = parse_time_value(inner.next().unwrap())?;
189    let end = parse_time_value(inner.next().unwrap())?;
190    let label = extract_string_literal(&inner.next().unwrap());
191    let props = build_block_options(inner.next().unwrap())?;
192
193    Ok(SpanDecl {
194        lane_ref,
195        start,
196        end,
197        label,
198        props,
199    })
200}
201
202// ─── Event ──────────────────────────────────────────────────
203
204fn build_event(pair: Pair<'_, Rule>) -> Result<EventDecl> {
205    let mut inner = pair.into_inner();
206    let lane_ref = inner.next().unwrap().as_str().to_string();
207    let time = parse_time_value(inner.next().unwrap())?;
208    let label = extract_string_literal(&inner.next().unwrap());
209    let props = build_block_options(inner.next().unwrap())?;
210
211    Ok(EventDecl {
212        lane_ref,
213        time,
214        label,
215        props,
216    })
217}
218
219// ─── Event Range ────────────────────────────────────────────
220
221fn build_event_range(pair: Pair<'_, Rule>) -> Result<EventRangeDecl> {
222    let mut inner = pair.into_inner();
223    let lane_ref = inner.next().unwrap().as_str().to_string();
224    let start = parse_time_value(inner.next().unwrap())?;
225    let end = parse_time_value(inner.next().unwrap())?;
226    let label = extract_string_literal(&inner.next().unwrap());
227    let props = build_block_options(inner.next().unwrap())?;
228
229    Ok(EventRangeDecl {
230        lane_ref,
231        start,
232        end,
233        label,
234        props,
235    })
236}
237
238// ─── Block Options ──────────────────────────────────────────
239
240fn build_block_options(pair: Pair<'_, Rule>) -> Result<ItemProps> {
241    let mut props = ItemProps::default();
242    for item in pair.into_inner() {
243        match item.as_rule() {
244            Rule::tags_option => {
245                props.tags = build_string_list(item.into_inner().next().unwrap());
246            }
247            Rule::source_option => {
248                props.source = Some(build_source_ref(item.into_inner().next().unwrap()));
249            }
250            Rule::id_option => {
251                props.id = Some(extract_string_literal(&item.into_inner().next().unwrap()));
252            }
253            Rule::origin_option => {
254                props.origin = Some(item.into_inner().next().unwrap().as_str().to_string());
255            }
256            _ => {}
257        }
258    }
259    Ok(props)
260}
261
262// ─── Import ─────────────────────────────────────────────────
263
264fn build_import(pair: Pair<'_, Rule>) -> Result<ImportBlock> {
265    let mut inner = pair.into_inner();
266
267    let source_type = inner
268        .next()
269        .unwrap()
270        .into_inner()
271        .next()
272        .unwrap()
273        .as_str()
274        .to_string();
275
276    let mut alias = None;
277    let mut items = Vec::new();
278    let mut policy = None;
279
280    for item in inner {
281        match item.as_rule() {
282            Rule::ident => {
283                alias = Some(item.as_str().to_string());
284            }
285            Rule::entity_import => {
286                let mut ei = item.into_inner();
287                let qid = ei.next().unwrap().as_str().to_string();
288                let entity_alias = ei.next().map(|p| p.as_str().to_string());
289                items.push(ImportItem::Entity {
290                    qid,
291                    alias: entity_alias,
292                });
293            }
294            Rule::query_import => {
295                let mut qi = item.into_inner();
296                let query = extract_string_literal(&qi.next().unwrap());
297                let query_alias = qi.next().map(|p| p.as_str().to_string());
298                items.push(ImportItem::Query {
299                    query,
300                    alias: query_alias,
301                });
302            }
303            Rule::policy_import => {
304                let name = item.into_inner().next().unwrap().as_str();
305                policy = Some(match name {
306                    "merge_by_source" => ReimportPolicy::MergeBySource,
307                    "overwrite_imported" => ReimportPolicy::OverwriteImported,
308                    "keep_manual" => ReimportPolicy::KeepManual,
309                    other => return Err(ParseError::UnknownPolicy(other.to_string())),
310                });
311            }
312            Rule::field_priority_policy => {
313                let mut config = FieldPriorityConfig::default();
314                for decl in item.into_inner() {
315                    let rule = decl.as_rule();
316                    let strategy_str = decl.into_inner().next().unwrap().as_str();
317                    let strategy = match strategy_str {
318                        "manual" => FieldStrategy::Manual,
319                        "wikidata" => FieldStrategy::Wikidata,
320                        "merge" => FieldStrategy::Merge,
321                        _ => unreachable!("grammar enforces valid values"),
322                    };
323                    match rule {
324                        Rule::label_strategy => config.label = strategy,
325                        Rule::time_strategy => config.time = strategy,
326                        Rule::tags_strategy => config.tags = strategy,
327                        _ => {}
328                    }
329                }
330                policy = Some(ReimportPolicy::FieldPriority(config));
331            }
332            _ => {}
333        }
334    }
335
336    Ok(ImportBlock {
337        source_type,
338        alias,
339        items,
340        policy,
341    })
342}
343
344// ─── Template ───────────────────────────────────────────────
345
346fn build_template(pair: Pair<'_, Rule>) -> Result<TemplateBlock> {
347    let mut inner = pair.into_inner();
348    let name = extract_string_literal(&inner.next().unwrap());
349
350    // Optional alias: if the next token is an ident before target_type
351    let mut alias = None;
352    let mut peeked = inner.next().unwrap();
353    if peeked.as_rule() == Rule::ident {
354        alias = Some(peeked.as_str().to_string());
355        peeked = inner.next().unwrap();
356    }
357
358    // peeked is now target_type
359    let target_type = parse_target_type(peeked.into_inner().next().unwrap())?;
360
361    let mut props = Vec::new();
362    for item in inner {
363        build_map_prop(item, &mut props)?;
364    }
365
366    Ok(TemplateBlock {
367        name,
368        alias,
369        target_type,
370        props,
371    })
372}
373
374// ─── Apply ──────────────────────────────────────────────────
375
376fn build_apply(pair: Pair<'_, Rule>) -> Result<ApplyBlock> {
377    let mut inner = pair.into_inner();
378    let template_alias = inner.next().unwrap().as_str().to_string();
379    let import_alias = inner.next().unwrap().as_str().to_string();
380
381    let mut overrides = Vec::new();
382    for item in inner {
383        build_map_prop(item, &mut overrides)?;
384    }
385
386    Ok(ApplyBlock {
387        template_alias,
388        import_alias,
389        overrides,
390    })
391}
392
393// ─── Map ────────────────────────────────────────────────────
394
395fn build_map(pair: Pair<'_, Rule>) -> Result<MapBlock> {
396    let mut inner = pair.into_inner();
397    let source_ref = inner.next().unwrap().as_str().to_string();
398    let target_type = parse_target_type(inner.next().unwrap().into_inner().next().unwrap())?;
399
400    let mut props = Vec::new();
401    for item in inner {
402        build_map_prop(item, &mut props)?;
403    }
404
405    Ok(MapBlock {
406        source_ref,
407        target_type,
408        props,
409    })
410}
411
412fn build_map_prop(item: Pair<'_, Rule>, props: &mut Vec<MapProp>) -> Result<()> {
413    match item.as_rule() {
414        Rule::map_lane => {
415            props.push(MapProp::Lane(
416                item.into_inner().next().unwrap().as_str().to_string(),
417            ));
418        }
419        Rule::map_start => {
420            props.push(MapProp::Start(build_map_expr(
421                item.into_inner().next().unwrap(),
422            )?));
423        }
424        Rule::map_end => {
425            props.push(MapProp::End(build_map_expr(
426                item.into_inner().next().unwrap(),
427            )?));
428        }
429        Rule::map_time => {
430            props.push(MapProp::Time(build_map_expr(
431                item.into_inner().next().unwrap(),
432            )?));
433        }
434        Rule::map_label => {
435            props.push(MapProp::Label(build_label_expr(
436                item.into_inner().next().unwrap(),
437            )));
438        }
439        Rule::map_tags => {
440            props.push(MapProp::Tags(build_string_list(
441                item.into_inner().next().unwrap(),
442            )));
443        }
444        Rule::map_filter => {
445            props.push(MapProp::Filter(build_filter_expr(
446                item.into_inner().next().unwrap(),
447            )?));
448        }
449        Rule::map_expand => {
450            // map_expand = { "expand" ~ claim_call ~ ";" }
451            let claim_pair = item.into_inner().next().unwrap();
452            let property = claim_pair.into_inner().next().unwrap().as_str().to_string();
453            props.push(MapProp::Expand(ClaimCall { property }));
454        }
455        _ => {}
456    }
457    Ok(())
458}
459
460// ─── Filter expressions ─────────────────────────────────────
461
462fn build_filter_expr(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
463    // filter_expr wraps filter_or
464    let inner = pair.into_inner().next().unwrap();
465    build_filter_or(inner)
466}
467
468fn build_filter_or(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
469    let mut iter = pair.into_inner();
470    let first = build_filter_and(iter.next().unwrap())?;
471    iter.try_fold(first, |acc, p| {
472        let rhs = build_filter_and(p)?;
473        Ok(FilterExpr::Or(Box::new(acc), Box::new(rhs)))
474    })
475}
476
477fn build_filter_and(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
478    let mut iter = pair.into_inner();
479    let first = build_filter_not(iter.next().unwrap())?;
480    iter.try_fold(first, |acc, p| {
481        let rhs = build_filter_not(p)?;
482        Ok(FilterExpr::And(Box::new(acc), Box::new(rhs)))
483    })
484}
485
486fn build_filter_not(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
487    let s = pair.as_str();
488    let inner = pair.into_inner().next().unwrap();
489    let atom = build_filter_atom(inner)?;
490    if s.trim_start().starts_with('!') {
491        Ok(FilterExpr::Not(Box::new(atom)))
492    } else {
493        Ok(atom)
494    }
495}
496
497fn build_filter_atom(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
498    let location = pair_location_str(&pair);
499    match pair.as_rule() {
500        Rule::filter_paren => build_filter_expr(pair.into_inner().next().unwrap()),
501        Rule::filter_compare => build_filter_compare(pair),
502        Rule::filter_string_op => build_filter_string_op(pair),
503        other => Err(ParseError::UnexpectedRule {
504            rule: format!("filter_atom: {other:?}"),
505            location,
506        }),
507    }
508}
509
510fn build_filter_string_op(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
511    let mut iter = pair.into_inner();
512    let label_ref_pair = iter.next().unwrap();
513    let lang = label_ref_pair
514        .as_str()
515        .strip_prefix("label@")
516        .unwrap_or(label_ref_pair.as_str())
517        .to_string();
518    let op_pair = iter.next().unwrap();
519    let location = pair_location_str(&op_pair);
520    let op = match op_pair.as_str() {
521        "contains" => StringMatchOp::Contains,
522        "startswith" => StringMatchOp::StartsWith,
523        other => {
524            return Err(ParseError::UnexpectedRule {
525                rule: format!("string_match_op: {other}"),
526                location,
527            });
528        }
529    };
530    let rhs_pair = iter.next().unwrap();
531    let rhs = extract_string_literal(&rhs_pair);
532    Ok(FilterExpr::StringMatch {
533        lhs: LabelRef { lang },
534        op,
535        rhs,
536    })
537}
538
539fn build_filter_compare(pair: Pair<'_, Rule>) -> Result<FilterExpr> {
540    let mut iter = pair.into_inner();
541    let lhs = build_filter_operand(iter.next().unwrap())?;
542    let op_pair = iter.next().unwrap();
543    let op_location = pair_location_str(&op_pair);
544    let op = parse_compare_op(op_pair.as_str(), op_location)?;
545    let rhs = build_filter_operand(iter.next().unwrap())?;
546    Ok(FilterExpr::Compare { lhs, op, rhs })
547}
548
549fn build_filter_operand(pair: Pair<'_, Rule>) -> Result<FilterOperand> {
550    let location = pair_location_str(&pair);
551    match pair.as_rule() {
552        Rule::null_literal => Ok(FilterOperand::Null),
553        Rule::claim_expr => Ok(FilterOperand::Claim(build_claim_expr(pair)?)),
554        Rule::integer => {
555            let raw = pair.as_str().to_string();
556            let n: i64 = raw.parse().map_err(|_| ParseError::InvalidInt {
557                value: raw,
558                location,
559            })?;
560            Ok(FilterOperand::Int(n))
561        }
562        other => Err(ParseError::UnexpectedRule {
563            rule: format!("filter_operand: {other:?}"),
564            location,
565        }),
566    }
567}
568
569fn parse_compare_op(s: &str, location: String) -> Result<CompareOp> {
570    match s {
571        "==" => Ok(CompareOp::Eq),
572        "!=" => Ok(CompareOp::NotEq),
573        "<" => Ok(CompareOp::Lt),
574        "<=" => Ok(CompareOp::Le),
575        ">" => Ok(CompareOp::Gt),
576        ">=" => Ok(CompareOp::Ge),
577        other => Err(ParseError::UnexpectedRule {
578            rule: format!("compare_op: {other}"),
579            location,
580        }),
581    }
582}
583
584fn build_map_expr(pair: Pair<'_, Rule>) -> Result<MapExpr> {
585    let mut fallbacks = Vec::new();
586    for child in pair.into_inner() {
587        let fb = match child.as_rule() {
588            Rule::claim_expr => MapFallback::Claim(build_claim_expr(child)?),
589            Rule::integer => {
590                let span = child.as_span();
591                let n: i64 = child.as_str().parse().map_err(|_| ParseError::InvalidInt {
592                    value: child.as_str().to_string(),
593                    location: format!("{}:{}", span.start(), span.end()),
594                })?;
595                MapFallback::Literal(n)
596            }
597            _ => {
598                let span = child.as_span();
599                return Err(ParseError::UnexpectedRule {
600                    rule: format!("{:?}", child.as_rule()),
601                    location: format!("{}:{}", span.start(), span.end()),
602                });
603            }
604        };
605        fallbacks.push(fb);
606    }
607    Ok(MapExpr { fallbacks })
608}
609
610fn build_claim_expr(pair: Pair<'_, Rule>) -> Result<ClaimExpr> {
611    let mut inner = pair.into_inner();
612    let claim_pair = inner.next().unwrap();
613    let property = claim_pair.into_inner().next().unwrap().as_str().to_string();
614
615    let mut qualifier = None;
616    let mut accessor = None;
617    let mut offset = None;
618    for child in inner {
619        match child.as_rule() {
620            Rule::claim_qualifier => {
621                // claim_qualifier = { "." ~ "qualifier" ~ "(" ~ property_id ~ ")" }
622                qualifier = Some(child.into_inner().next().unwrap().as_str().to_string());
623            }
624            Rule::claim_accessor => {
625                // claim_accessor = { "." ~ ident }
626                accessor = Some(child.into_inner().next().unwrap().as_str().to_string());
627            }
628            Rule::claim_offset => {
629                // as_str() gives "+1" or "-30"; parse directly as i32
630                offset = child.as_str().parse::<i32>().ok();
631            }
632            _ => {}
633        }
634    }
635
636    Ok(ClaimExpr {
637        claim: ClaimCall { property },
638        qualifier,
639        accessor,
640        offset,
641    })
642}
643
644fn build_label_expr(pair: Pair<'_, Rule>) -> LabelExpr {
645    let fallbacks = pair
646        .into_inner()
647        .map(|lr| {
648            // label_ref is compound: "label" "@" ident — the whole thing is captured
649            let s = lr.as_str();
650            let lang = s.strip_prefix("label@").unwrap_or(s).to_string();
651            LabelRef { lang }
652        })
653        .collect();
654    LabelExpr { fallbacks }
655}
656
657fn parse_target_type(pair: Pair<'_, Rule>) -> Result<MapTargetType> {
658    match pair.as_str() {
659        "span" => Ok(MapTargetType::Span),
660        "event" => Ok(MapTargetType::Event),
661        "event_range" => Ok(MapTargetType::EventRange),
662        other => Err(ParseError::UnknownTargetType(other.to_string())),
663    }
664}
665
666// ─── Helpers ────────────────────────────────────────────────
667
668fn extract_string_literal(pair: &Pair<'_, Rule>) -> String {
669    let inner = pair.clone().into_inner().next().unwrap();
670    let s = inner.as_str();
671    let mut out = String::with_capacity(s.len());
672    let mut chars = s.chars();
673    while let Some(c) = chars.next() {
674        if c == '\\' {
675            match chars.next() {
676                Some('"') => out.push('"'),
677                Some('\\') => out.push('\\'),
678                Some(c) => {
679                    out.push('\\');
680                    out.push(c);
681                }
682                None => out.push('\\'),
683            }
684        } else {
685            out.push(c);
686        }
687    }
688    out
689}
690
691fn build_source_ref(pair: Pair<'_, Rule>) -> SourceRef {
692    let s = pair.as_str();
693    let (prefix, qid) = s.split_once(':').unwrap();
694    SourceRef {
695        prefix: prefix.to_string(),
696        qid: qid.to_string(),
697    }
698}
699
700fn build_string_list(pair: Pair<'_, Rule>) -> Vec<String> {
701    pair.into_inner()
702        .map(|p| extract_string_literal(&p))
703        .collect()
704}
705
706fn parse_int(pair: &Pair<'_, Rule>) -> Result<i64> {
707    pair.as_str()
708        .parse::<i64>()
709        .map_err(|_| ParseError::InvalidInt {
710            value: pair.as_str().to_string(),
711            location: format!("{}:{}", pair.as_span().start(), pair.as_span().end()),
712        })
713}
714
715/// `time_value` ノードを [`TimeValue`] に変換する。
716///
717/// PEG ルールは `date_lit | year_month_lit | year_lit` の順に試行され、
718/// 月は 1〜12、日は 1〜31 の範囲を builder 側で検証する。
719/// カレンダー妥当性(2月30日など)は lowering 側の責務。
720pub(crate) fn parse_time_value(pair: Pair<'_, Rule>) -> Result<TimeValue> {
721    let location = pair_location_str(&pair);
722    let inner = pair
723        .into_inner()
724        .next()
725        .ok_or_else(|| ParseError::UnexpectedRule {
726            rule: "time_value: empty inner".to_string(),
727            location: location.clone(),
728        })?;
729    match inner.as_rule() {
730        Rule::year_lit => {
731            let year = inner
732                .as_str()
733                .parse::<i64>()
734                .map_err(|_| ParseError::InvalidInt {
735                    value: inner.as_str().to_string(),
736                    location: location.clone(),
737                })?;
738            Ok(TimeValue::Year(year))
739        }
740        Rule::year_month_lit => {
741            let s = inner.as_str();
742            let (y, m) = s
743                .split_once('-')
744                .ok_or_else(|| ParseError::UnexpectedRule {
745                    rule: format!("year_month_lit malformed: {s}"),
746                    location: location.clone(),
747                })?;
748            let year = y.parse::<i64>().map_err(|_| ParseError::InvalidInt {
749                value: y.to_string(),
750                location: location.clone(),
751            })?;
752            let month: u32 = m.parse().map_err(|_| ParseError::InvalidInt {
753                value: m.to_string(),
754                location: location.clone(),
755            })?;
756            check_month(month, &location)?;
757            Ok(TimeValue::YearMonth(year, month as u8))
758        }
759        Rule::date_lit => {
760            let s = inner.as_str();
761            let mut parts = s.splitn(3, '-');
762            let y = parts.next().ok_or_else(|| ParseError::UnexpectedRule {
763                rule: format!("date_lit malformed: {s}"),
764                location: location.clone(),
765            })?;
766            let m = parts.next().ok_or_else(|| ParseError::UnexpectedRule {
767                rule: format!("date_lit malformed: {s}"),
768                location: location.clone(),
769            })?;
770            let d = parts.next().ok_or_else(|| ParseError::UnexpectedRule {
771                rule: format!("date_lit malformed: {s}"),
772                location: location.clone(),
773            })?;
774            let year = y.parse::<i64>().map_err(|_| ParseError::InvalidInt {
775                value: y.to_string(),
776                location: location.clone(),
777            })?;
778            let month: u32 = m.parse().map_err(|_| ParseError::InvalidInt {
779                value: m.to_string(),
780                location: location.clone(),
781            })?;
782            let day: u32 = d.parse().map_err(|_| ParseError::InvalidInt {
783                value: d.to_string(),
784                location: location.clone(),
785            })?;
786            check_month(month, &location)?;
787            check_day(day, &location)?;
788            Ok(TimeValue::Date(year, month as u8, day as u8))
789        }
790        other => Err(ParseError::UnexpectedRule {
791            rule: format!("time_value: {other:?}"),
792            location,
793        }),
794    }
795}
796
797fn check_month(month: u32, location: &str) -> Result<()> {
798    if (1..=12).contains(&month) {
799        Ok(())
800    } else {
801        Err(ParseError::InvalidMonth {
802            value: month,
803            location: location.to_string(),
804        })
805    }
806}
807
808fn check_day(day: u32, location: &str) -> Result<()> {
809    if (1..=31).contains(&day) {
810        Ok(())
811    } else {
812        Err(ParseError::InvalidDay {
813            value: day,
814            location: location.to_string(),
815        })
816    }
817}
818
819fn pair_span(pair: &Pair<'_, Rule>) -> Span {
820    let s = pair.as_span();
821    Span {
822        start: s.start(),
823        end: s.end(),
824    }
825}
826
827fn pair_location_str(pair: &Pair<'_, Rule>) -> String {
828    let s = pair.as_span();
829    format!("{}:{}", s.start(), s.end())
830}