Skip to main content

tdsl_parser/
lib.rs

1pub mod ast;
2pub mod builder;
3pub mod error;
4pub mod format;
5
6pub use error::{ParseDiagnostic, byte_offset_to_line_col};
7pub use format::{format_file, format_source};
8
9use pest::Parser;
10use pest_derive::Parser;
11
12#[derive(Parser)]
13#[grammar = "grammar.pest"]
14struct TdslParser;
15
16/// Parse a DSL source string into an AST [`ast::File`].
17pub fn parse(source: &str) -> Result<ast::File, error::ParseError> {
18    let pairs = TdslParser::parse(Rule::file, source)?;
19    builder::build_file(pairs)
20}
21
22/// 単独の時刻リテラル文字列を [`ast::TimeValue`] にパースする。
23///
24/// `YYYY-MM-DD` / `YYYY-MM` / `YYYY` の 3 形式に対応し、月 1〜12 / 日 1〜31
25/// の範囲外は `ParseError` を返す。前後の空白は許容して除去するが、
26/// 文字列内部に余計なトークンがある場合は拒否する。
27/// `tdsl import-csv` など、DSL 本文の外部で時刻文字列を解釈する経路から利用する。
28pub fn parse_time_literal(s: &str) -> Result<ast::TimeValue, error::ParseError> {
29    let trimmed = s.trim();
30    let mut pairs = TdslParser::parse(Rule::time_literal_only, trimmed)?;
31    let outer = pairs
32        .next()
33        .ok_or_else(|| error::ParseError::UnexpectedRule {
34            rule: "time_literal_only: empty".to_string(),
35            location: "0:0".to_string(),
36        })?;
37    let time_value_pair = outer
38        .into_inner()
39        .find(|p| matches!(p.as_rule(), Rule::time_value))
40        .ok_or_else(|| error::ParseError::UnexpectedRule {
41            rule: "time_literal_only: missing time_value".to_string(),
42            location: "0:0".to_string(),
43        })?;
44    builder::parse_time_value(time_value_pair)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn parse_timeline_block() {
53        let src = r#"
54            timeline "中国王朝年表" {
55                title "中国王朝年表";
56                unit year;
57                range -500..2000;
58                calendar proleptic_gregorian;
59            }
60        "#;
61        let file = parse(src).unwrap();
62        assert_eq!(file.statements.len(), 1);
63        match &file.statements[0].node {
64            ast::Statement::Timeline(t) => {
65                assert_eq!(t.name, "中国王朝年表");
66                assert_eq!(t.title.as_deref(), Some("中国王朝年表"));
67                assert_eq!(t.unit.as_deref(), Some("year"));
68                assert_eq!(
69                    t.range,
70                    Some(ast::RangeExpr {
71                        start: ast::TimeValue::Year(-500),
72                        end: ast::TimeValue::Year(2000),
73                    })
74                );
75                assert_eq!(t.calendar.as_deref(), Some("proleptic_gregorian"));
76            }
77            _ => panic!("expected Timeline"),
78        }
79    }
80
81    #[test]
82    fn parse_lane() {
83        let src = r#"lane "漢" as han { kind dynasty; order 10; }"#;
84        let file = parse(src).unwrap();
85        assert_eq!(file.statements.len(), 1);
86        match &file.statements[0].node {
87            ast::Statement::Lane(l) => {
88                assert_eq!(l.label, "漢");
89                assert_eq!(l.alias.as_deref(), Some("han"));
90                assert_eq!(l.kind.as_deref(), Some("dynasty"));
91                assert_eq!(l.order, Some(10));
92            }
93            _ => panic!("expected Lane"),
94        }
95    }
96
97    #[test]
98    fn parse_span() {
99        let src =
100            r#"span han -206..220 "漢" { tags ["dynasty"]; source wd:Q7209; id "span:han"; };"#;
101        let file = parse(src).unwrap();
102        assert_eq!(file.statements.len(), 1);
103        match &file.statements[0].node {
104            ast::Statement::Span(s) => {
105                assert_eq!(s.lane_ref, "han");
106                assert_eq!(s.start, ast::TimeValue::Year(-206));
107                assert_eq!(s.end, ast::TimeValue::Year(220));
108                assert_eq!(s.label, "漢");
109                assert_eq!(s.props.tags, vec!["dynasty"]);
110                assert_eq!(
111                    s.props.source,
112                    Some(ast::SourceRef {
113                        prefix: "wd".to_string(),
114                        qid: "Q7209".to_string(),
115                    })
116                );
117                assert_eq!(s.props.id.as_deref(), Some("span:han"));
118            }
119            _ => panic!("expected Span"),
120        }
121    }
122
123    #[test]
124    fn parse_event() {
125        let src = r#"event han -209 "陳勝・呉広の乱" {};"#;
126        let file = parse(src).unwrap();
127        assert_eq!(file.statements.len(), 1);
128        match &file.statements[0].node {
129            ast::Statement::Event(e) => {
130                assert_eq!(e.lane_ref, "han");
131                assert_eq!(e.time, ast::TimeValue::Year(-209));
132                assert_eq!(e.label, "陳勝・呉広の乱");
133            }
134            _ => panic!("expected Event"),
135        }
136    }
137
138    #[test]
139    fn parse_event_range() {
140        let src = r#"event_range han 184..204 "黄巾の乱" { tags ["war"]; };"#;
141        let file = parse(src).unwrap();
142        assert_eq!(file.statements.len(), 1);
143        match &file.statements[0].node {
144            ast::Statement::EventRange(er) => {
145                assert_eq!(er.lane_ref, "han");
146                assert_eq!(er.start, ast::TimeValue::Year(184));
147                assert_eq!(er.end, ast::TimeValue::Year(204));
148                assert_eq!(er.label, "黄巾の乱");
149                assert_eq!(er.props.tags, vec!["war"]);
150            }
151            _ => panic!("expected EventRange"),
152        }
153    }
154
155    #[test]
156    fn parse_import_block() {
157        let src = r#"
158            import wikidata as wd {
159                entity Q7209 as han_dynasty;
160                query "SELECT ?item WHERE { ?item wdt:P31 wd:Q28171280 . }" as dynasties;
161                policy merge_by_source;
162            }
163        "#;
164        let file = parse(src).unwrap();
165        assert_eq!(file.statements.len(), 1);
166        match &file.statements[0].node {
167            ast::Statement::Import(imp) => {
168                assert_eq!(imp.source_type, "wikidata");
169                assert_eq!(imp.alias.as_deref(), Some("wd"));
170                assert_eq!(imp.items.len(), 2);
171                assert!(matches!(
172                    &imp.items[0],
173                    ast::ImportItem::Entity { qid, alias }
174                        if qid == "Q7209" && alias.as_deref() == Some("han_dynasty")
175                ));
176                assert!(matches!(
177                    &imp.items[1],
178                    ast::ImportItem::Query { query, alias }
179                        if query.contains("P31") && alias.as_deref() == Some("dynasties")
180                ));
181                assert_eq!(imp.policy, Some(ast::ReimportPolicy::MergeBySource));
182            }
183            _ => panic!("expected Import"),
184        }
185    }
186
187    #[test]
188    fn parse_map_block() {
189        let src = r#"
190            map wd.han_dynasty to span {
191                lane han;
192                start claim(P571).year;
193                end claim(P576).year;
194                label label@ja ?? label@en;
195            }
196        "#;
197        let file = parse(src).unwrap();
198        assert_eq!(file.statements.len(), 1);
199        match &file.statements[0].node {
200            ast::Statement::Map(m) => {
201                assert_eq!(m.source_ref, "wd.han_dynasty");
202                assert_eq!(m.target_type, ast::MapTargetType::Span);
203                assert_eq!(m.props.len(), 4);
204            }
205            _ => panic!("expected Map"),
206        }
207    }
208
209    #[test]
210    fn parse_map_expr_with_fallback() {
211        let src = r#"
212            map wd.han to span {
213                lane han;
214                start claim(P580).year ?? claim(P571).year;
215                end claim(P582).year ?? claim(P576).year;
216                label label@ja ?? label@en;
217            }
218        "#;
219        let file = parse(src).unwrap();
220        match &file.statements[0].node {
221            ast::Statement::Map(m) => {
222                let start = m
223                    .props
224                    .iter()
225                    .find_map(|p| match p {
226                        ast::MapProp::Start(e) => Some(e),
227                        _ => None,
228                    })
229                    .expect("start present");
230                assert_eq!(start.fallbacks.len(), 2);
231                match &start.fallbacks[0] {
232                    ast::MapFallback::Claim(c) => {
233                        assert_eq!(c.claim.property, "P580");
234                        assert_eq!(c.accessor.as_deref(), Some("year"));
235                    }
236                    _ => panic!("expected Claim"),
237                }
238                match &start.fallbacks[1] {
239                    ast::MapFallback::Claim(c) => {
240                        assert_eq!(c.claim.property, "P571");
241                        assert_eq!(c.accessor.as_deref(), Some("year"));
242                    }
243                    _ => panic!("expected Claim"),
244                }
245
246                let end = m
247                    .props
248                    .iter()
249                    .find_map(|p| match p {
250                        ast::MapProp::End(e) => Some(e),
251                        _ => None,
252                    })
253                    .expect("end present");
254                assert_eq!(end.fallbacks.len(), 2);
255                match &end.fallbacks[0] {
256                    ast::MapFallback::Claim(c) => assert_eq!(c.claim.property, "P582"),
257                    _ => panic!("expected Claim"),
258                }
259                match &end.fallbacks[1] {
260                    ast::MapFallback::Claim(c) => assert_eq!(c.claim.property, "P576"),
261                    _ => panic!("expected Claim"),
262                }
263            }
264            _ => panic!("expected Map"),
265        }
266    }
267
268    #[test]
269    fn parse_map_expr_single_claim_still_works() {
270        let src = r#"
271            map wd.x to event {
272                lane a;
273                time claim(P571).year;
274                label label@ja;
275            }
276        "#;
277        let file = parse(src).unwrap();
278        match &file.statements[0].node {
279            ast::Statement::Map(m) => {
280                let time = m
281                    .props
282                    .iter()
283                    .find_map(|p| match p {
284                        ast::MapProp::Time(e) => Some(e),
285                        _ => None,
286                    })
287                    .expect("time present");
288                assert_eq!(time.fallbacks.len(), 1);
289                match &time.fallbacks[0] {
290                    ast::MapFallback::Claim(c) => {
291                        assert_eq!(c.claim.property, "P571");
292                        assert_eq!(c.accessor.as_deref(), Some("year"));
293                    }
294                    _ => panic!("expected Claim"),
295                }
296            }
297            _ => panic!("expected Map"),
298        }
299    }
300
301    #[test]
302    fn parse_map_expr_three_fallbacks() {
303        let src = r#"
304            map wd.x to event {
305                lane a;
306                time claim(P580).year ?? claim(P571).year ?? claim(P569).year;
307                label label@ja;
308            }
309        "#;
310        let file = parse(src).unwrap();
311        match &file.statements[0].node {
312            ast::Statement::Map(m) => {
313                let time = m
314                    .props
315                    .iter()
316                    .find_map(|p| match p {
317                        ast::MapProp::Time(e) => Some(e),
318                        _ => None,
319                    })
320                    .expect("time present");
321                assert_eq!(time.fallbacks.len(), 3);
322                for (fb, expected) in time.fallbacks.iter().zip(["P580", "P571", "P569"]) {
323                    match fb {
324                        ast::MapFallback::Claim(c) => assert_eq!(c.claim.property, expected),
325                        _ => panic!("expected Claim"),
326                    }
327                }
328            }
329            _ => panic!("expected Map"),
330        }
331    }
332
333    fn extract_map_filters(file: &ast::File) -> Vec<ast::FilterExpr> {
334        match &file.statements[0].node {
335            ast::Statement::Map(m) => m
336                .props
337                .iter()
338                .filter_map(|p| match p {
339                    ast::MapProp::Filter(e) => Some(e.clone()),
340                    _ => None,
341                })
342                .collect(),
343            _ => panic!("expected Map"),
344        }
345    }
346
347    #[test]
348    fn parse_map_filter_basic_gt() {
349        let src = r#"
350            map wd.x to span {
351                lane a;
352                filter claim(P580).year > 1000;
353                start claim(P580).year;
354                end claim(P582).year;
355                label label@ja;
356            }
357        "#;
358        let file = parse(src).unwrap();
359        let filters = extract_map_filters(&file);
360        assert_eq!(filters.len(), 1);
361        match &filters[0] {
362            ast::FilterExpr::Compare { lhs, op, rhs } => {
363                assert!(matches!(lhs, ast::FilterOperand::Claim(_)));
364                assert_eq!(*op, ast::CompareOp::Gt);
365                assert!(matches!(rhs, ast::FilterOperand::Int(1000)));
366            }
367            other => panic!("expected Compare, got {other:?}"),
368        }
369    }
370
371    #[test]
372    fn parse_map_filter_null_check() {
373        let src = r#"
374            map wd.x to span {
375                lane a;
376                filter claim(P576).year != null;
377                start claim(P580).year;
378                end claim(P576).year;
379                label label@ja;
380            }
381        "#;
382        let file = parse(src).unwrap();
383        let filters = extract_map_filters(&file);
384        assert_eq!(filters.len(), 1);
385        match &filters[0] {
386            ast::FilterExpr::Compare { lhs, op, rhs } => {
387                assert!(matches!(lhs, ast::FilterOperand::Claim(_)));
388                assert_eq!(*op, ast::CompareOp::NotEq);
389                assert!(matches!(rhs, ast::FilterOperand::Null));
390            }
391            other => panic!("expected Compare, got {other:?}"),
392        }
393    }
394
395    #[test]
396    fn parse_map_filter_and_or() {
397        let src = r#"
398            map wd.x to span {
399                lane a;
400                filter claim(P580).year > 1000 && claim(P582).year < 2000;
401                start claim(P580).year;
402                end claim(P582).year;
403                label label@ja;
404            }
405        "#;
406        let file = parse(src).unwrap();
407        let filters = extract_map_filters(&file);
408        assert_eq!(filters.len(), 1);
409        assert!(matches!(&filters[0], ast::FilterExpr::And(_, _)));
410    }
411
412    #[test]
413    fn parse_map_filter_not() {
414        let src = r#"
415            map wd.x to span {
416                lane a;
417                filter !(claim(P582).year == null);
418                start claim(P580).year;
419                end claim(P582).year;
420                label label@ja;
421            }
422        "#;
423        let file = parse(src).unwrap();
424        let filters = extract_map_filters(&file);
425        assert_eq!(filters.len(), 1);
426        match &filters[0] {
427            ast::FilterExpr::Not(inner) => {
428                assert!(matches!(inner.as_ref(), ast::FilterExpr::Compare { .. }));
429            }
430            other => panic!("expected Not, got {other:?}"),
431        }
432    }
433
434    #[test]
435    fn parse_map_multiple_filters() {
436        let src = r#"
437            map wd.x to span {
438                lane a;
439                filter claim(P580).year > 1000;
440                filter claim(P576).year != null;
441                start claim(P580).year;
442                end claim(P576).year;
443                label label@ja;
444            }
445        "#;
446        let file = parse(src).unwrap();
447        let filters = extract_map_filters(&file);
448        assert_eq!(filters.len(), 2);
449    }
450
451    #[test]
452    fn parse_map_filter_paren_precedence() {
453        // (a > 1) || (b < 0 && c > 2)
454        let src = r#"
455            map wd.x to span {
456                lane a;
457                filter claim(P580).year > 1
458                    || (claim(P582).year < 0 && claim(P571).year > 2);
459                start claim(P580).year;
460                end claim(P582).year;
461                label label@ja;
462            }
463        "#;
464        let file = parse(src).unwrap();
465        let filters = extract_map_filters(&file);
466        assert_eq!(filters.len(), 1);
467        // Top-level should be Or, with the right side being And.
468        match &filters[0] {
469            ast::FilterExpr::Or(_lhs, rhs) => {
470                assert!(matches!(rhs.as_ref(), ast::FilterExpr::And(_, _)));
471            }
472            other => panic!("expected Or at top, got {other:?}"),
473        }
474    }
475
476    #[test]
477    fn parse_map_filter_string_contains() {
478        let src = r#"
479            map wd.x to span {
480                lane a;
481                filter label@ja contains "王朝";
482                start claim(P580).year;
483                end claim(P582).year;
484                label label@ja;
485            }
486        "#;
487        let file = parse(src).unwrap();
488        let filters = extract_map_filters(&file);
489        assert_eq!(filters.len(), 1);
490        match &filters[0] {
491            ast::FilterExpr::StringMatch { lhs, op, rhs } => {
492                assert_eq!(lhs.lang, "ja");
493                assert_eq!(*op, ast::StringMatchOp::Contains);
494                assert_eq!(rhs, "王朝");
495            }
496            other => panic!("expected StringMatch, got {other:?}"),
497        }
498    }
499
500    #[test]
501    fn parse_map_filter_string_startswith() {
502        let src = r#"
503            map wd.x to span {
504                lane a;
505                filter label@en startswith "Han";
506                start claim(P580).year;
507                end claim(P582).year;
508                label label@en;
509            }
510        "#;
511        let file = parse(src).unwrap();
512        let filters = extract_map_filters(&file);
513        assert_eq!(filters.len(), 1);
514        match &filters[0] {
515            ast::FilterExpr::StringMatch { lhs, op, rhs } => {
516                assert_eq!(lhs.lang, "en");
517                assert_eq!(*op, ast::StringMatchOp::StartsWith);
518                assert_eq!(rhs, "Han");
519            }
520            other => panic!("expected StringMatch, got {other:?}"),
521        }
522    }
523
524    #[test]
525    fn parse_map_filter_string_not_contains() {
526        let src = r#"
527            map wd.x to span {
528                lane a;
529                filter !(label@ja contains "候補");
530                start claim(P580).year;
531                end claim(P582).year;
532                label label@ja;
533            }
534        "#;
535        let file = parse(src).unwrap();
536        let filters = extract_map_filters(&file);
537        assert_eq!(filters.len(), 1);
538        match &filters[0] {
539            ast::FilterExpr::Not(inner) => {
540                assert!(matches!(
541                    inner.as_ref(),
542                    ast::FilterExpr::StringMatch { .. }
543                ));
544            }
545            other => panic!("expected Not(StringMatch), got {other:?}"),
546        }
547    }
548
549    #[test]
550    fn parse_map_filter_string_combined_with_numeric() {
551        let src = r#"
552            map wd.x to span {
553                lane a;
554                filter label@ja contains "王朝" && claim(P580).year > 0;
555                start claim(P580).year;
556                end claim(P582).year;
557                label label@ja;
558            }
559        "#;
560        let file = parse(src).unwrap();
561        let filters = extract_map_filters(&file);
562        assert_eq!(filters.len(), 1);
563        assert!(matches!(&filters[0], ast::FilterExpr::And(_, _)));
564    }
565
566    #[test]
567    fn parse_comments() {
568        let src = r#"
569            // This is a comment
570            lane "秦" as qin { kind dynasty; /* inline comment */ order 20; }
571        "#;
572        let file = parse(src).unwrap();
573        assert_eq!(file.statements.len(), 1);
574    }
575
576    #[test]
577    fn parse_full_example() {
578        let src = r#"
579            timeline "中国王朝年表" {
580                title "中国王朝年表";
581                unit year;
582                range -500..2000;
583                calendar proleptic_gregorian;
584            }
585
586            lane "漢" as han { kind dynasty; order 10; }
587            lane "秦" as qin { kind dynasty; order 20; }
588
589            span han -206..220 "漢" { tags ["dynasty"]; source wd:Q7209; id "span:han"; };
590            span qin -221..-206 "秦" { tags ["dynasty"]; source wd:Q7462; id "span:qin"; };
591
592            event han -209 "陳勝・呉広の乱" {};
593            event_range han 184..204 "黄巾の乱" { tags ["war"]; };
594
595            import wikidata as wd {
596                entity Q7209 as han_dynasty;
597                policy merge_by_source;
598            }
599
600            map wd.han_dynasty to span {
601                lane han;
602                start claim(P571).year;
603                end claim(P576).year;
604                label label@ja ?? label@en;
605            }
606        "#;
607        let file = parse(src).unwrap();
608        // timeline(1) + lanes(2) + spans(2) + event(1) + event_range(1) + import(1) + map(1) = 9
609        assert_eq!(file.statements.len(), 9);
610    }
611
612    #[test]
613    fn parse_unknown_target_type_fails() {
614        let src = r#"
615            map wd.x to unknown_type {
616                lane a;
617            }
618        "#;
619        let err = parse(src).expect_err("unknown target_type should fail to parse");
620        assert!(
621            matches!(&err, error::ParseError::UnknownTargetType(v) if v == "unknown_type"),
622            "expected UnknownTargetType(\"unknown_type\"), got: {err:?}"
623        );
624        // メッセージは許容値と実際の値の双方を提示する
625        let msg = err.to_string();
626        assert!(
627            msg.contains("unknown_type")
628                && msg.contains("span")
629                && msg.contains("event")
630                && msg.contains("event_range"),
631            "error message should list the invalid value and all allowed types, got: {msg}"
632        );
633    }
634
635    // ─── Edge cases ──────────────────────────────────────────────────────────
636
637    #[test]
638    fn parse_string_with_escaped_quote() {
639        let src = r#"lane "He said \"hello\"" as x {}"#;
640        let file = parse(src).unwrap();
641        match &file.statements[0].node {
642            ast::Statement::Lane(l) => {
643                assert!(l.label.contains("hello"));
644            }
645            _ => panic!("expected Lane"),
646        }
647    }
648
649    #[test]
650    fn parse_negative_boundary_values() {
651        let src = r#"span han -9999..0 "大昔" {};"#;
652        let file = parse(src).unwrap();
653        match &file.statements[0].node {
654            ast::Statement::Span(s) => {
655                assert_eq!(s.start, ast::TimeValue::Year(-9999));
656                assert_eq!(s.end, ast::TimeValue::Year(0));
657            }
658            _ => panic!("expected Span"),
659        }
660    }
661
662    #[test]
663    fn parse_multiple_tags_in_list() {
664        let src = r#"span han 100..200 "漢" { tags ["a", "b", "c"]; };"#;
665        let file = parse(src).unwrap();
666        match &file.statements[0].node {
667            ast::Statement::Span(s) => {
668                assert_eq!(s.props.tags, vec!["a", "b", "c"]);
669            }
670            _ => panic!("expected Span"),
671        }
672    }
673
674    #[test]
675    fn parse_event_range_with_id_and_origin() {
676        let src = r#"event_range han 100..200 "乱" { id "er:han:100"; origin manual; };"#;
677        let file = parse(src).unwrap();
678        match &file.statements[0].node {
679            ast::Statement::EventRange(er) => {
680                assert_eq!(er.props.id.as_deref(), Some("er:han:100"));
681                assert_eq!(er.props.origin.as_deref(), Some("manual"));
682            }
683            _ => panic!("expected EventRange"),
684        }
685    }
686
687    #[test]
688    fn parse_import_without_alias() {
689        let src = r#"
690            import wikidata {
691                entity Q7209;
692            }
693        "#;
694        let file = parse(src).unwrap();
695        match &file.statements[0].node {
696            ast::Statement::Import(imp) => {
697                assert_eq!(imp.source_type, "wikidata");
698                assert!(imp.alias.is_none());
699                assert_eq!(imp.items.len(), 1);
700                assert!(matches!(&imp.items[0],
701                    ast::ImportItem::Entity { qid, alias }
702                        if qid == "Q7209" && alias.is_none()
703                ));
704            }
705            _ => panic!("expected Import"),
706        }
707    }
708
709    #[test]
710    fn parse_map_with_tags() {
711        let src = r#"
712            map wd.han to span {
713                lane han;
714                start claim(P571).year;
715                end claim(P576).year;
716                label label@ja;
717                tags ["dynasty", "china"];
718            }
719        "#;
720        let file = parse(src).unwrap();
721        match &file.statements[0].node {
722            ast::Statement::Map(m) => {
723                let has_tags = m
724                    .props
725                    .iter()
726                    .any(|p| matches!(p, ast::MapProp::Tags(t) if t.len() == 2));
727                assert!(has_tags);
728            }
729            _ => panic!("expected Map"),
730        }
731    }
732
733    #[test]
734    fn parse_lane_without_alias_no_kind() {
735        let src = r#"lane "Simple" {}"#;
736        let file = parse(src).unwrap();
737        match &file.statements[0].node {
738            ast::Statement::Lane(l) => {
739                assert_eq!(l.label, "Simple");
740                assert!(l.alias.is_none());
741                assert!(l.kind.is_none());
742                assert!(l.order.is_none());
743            }
744            _ => panic!("expected Lane"),
745        }
746    }
747
748    #[test]
749    fn parse_block_comment_multiline() {
750        let src = r#"
751            /* This is a
752               multi-line
753               block comment */
754            lane "秦" as qin {}
755        "#;
756        let file = parse(src).unwrap();
757        assert_eq!(file.statements.len(), 1);
758        match &file.statements[0].node {
759            ast::Statement::Lane(l) => assert_eq!(l.label, "秦"),
760            _ => panic!("expected Lane"),
761        }
762    }
763
764    #[test]
765    fn parse_event_with_zero_year() {
766        let src = r#"event han 0 "年0の出来事" {};"#;
767        let file = parse(src).unwrap();
768        match &file.statements[0].node {
769            ast::Statement::Event(e) => assert_eq!(e.time, ast::TimeValue::Year(0)),
770            _ => panic!("expected Event"),
771        }
772    }
773
774    #[test]
775    fn parse_overwrite_imported_policy() {
776        let src = r#"import wikidata as wd { policy overwrite_imported; }"#;
777        let file = parse(src).unwrap();
778        match &file.statements[0].node {
779            ast::Statement::Import(imp) => {
780                assert_eq!(imp.policy, Some(ast::ReimportPolicy::OverwriteImported));
781            }
782            _ => panic!("expected Import"),
783        }
784    }
785
786    #[test]
787    fn parse_keep_manual_policy() {
788        let src = r#"import wikidata as wd { policy keep_manual; }"#;
789        let file = parse(src).unwrap();
790        match &file.statements[0].node {
791            ast::Statement::Import(imp) => {
792                assert_eq!(imp.policy, Some(ast::ReimportPolicy::KeepManual));
793            }
794            _ => panic!("expected Import"),
795        }
796    }
797
798    #[test]
799    fn parse_unknown_policy_fails() {
800        let src = r#"import wikidata as wd { policy unknown_policy; }"#;
801        let result = parse(src);
802        assert!(result.is_err());
803    }
804
805    #[test]
806    fn parse_template_block_with_alias() {
807        let src = r#"
808            template "人物の生涯" as person_life
809                to event_range {
810                    start claim(P569).year;
811                    end claim(P570).year;
812                    label label@ja ?? label@en;
813                }
814        "#;
815        let file = parse(src).unwrap();
816        assert_eq!(file.statements.len(), 1);
817        match &file.statements[0].node {
818            ast::Statement::Template(t) => {
819                assert_eq!(t.name, "人物の生涯");
820                assert_eq!(t.alias.as_deref(), Some("person_life"));
821                assert_eq!(t.target_type, ast::MapTargetType::EventRange);
822                assert_eq!(t.props.len(), 3);
823                assert!(matches!(&t.props[0], ast::MapProp::Start(_)));
824                assert!(matches!(&t.props[1], ast::MapProp::End(_)));
825                assert!(matches!(&t.props[2], ast::MapProp::Label(_)));
826            }
827            _ => panic!("expected Template"),
828        }
829    }
830
831    #[test]
832    fn parse_template_block_without_alias() {
833        let src = r#"
834            template "Dynasty Span"
835                to span {
836                    start claim(P571).year;
837                    end claim(P576).year;
838                    label label@ja ?? label@en;
839                }
840        "#;
841        let file = parse(src).unwrap();
842        assert_eq!(file.statements.len(), 1);
843        match &file.statements[0].node {
844            ast::Statement::Template(t) => {
845                assert_eq!(t.name, "Dynasty Span");
846                assert!(t.alias.is_none());
847                assert_eq!(t.target_type, ast::MapTargetType::Span);
848            }
849            _ => panic!("expected Template"),
850        }
851    }
852
853    #[test]
854    fn parse_apply_block_with_override() {
855        let src = r#"
856            apply person_life to emperors {
857                lane imperial;
858            }
859        "#;
860        let file = parse(src).unwrap();
861        assert_eq!(file.statements.len(), 1);
862        match &file.statements[0].node {
863            ast::Statement::Apply(a) => {
864                assert_eq!(a.template_alias, "person_life");
865                assert_eq!(a.import_alias, "emperors");
866                assert_eq!(a.overrides.len(), 1);
867                assert!(matches!(&a.overrides[0], ast::MapProp::Lane(id) if id == "imperial"));
868            }
869            _ => panic!("expected Apply"),
870        }
871    }
872
873    #[test]
874    fn parse_apply_block_empty_overrides() {
875        let src = r#"apply dynasty_template to imports {}"#;
876        let file = parse(src).unwrap();
877        assert_eq!(file.statements.len(), 1);
878        match &file.statements[0].node {
879            ast::Statement::Apply(a) => {
880                assert_eq!(a.template_alias, "dynasty_template");
881                assert_eq!(a.import_alias, "imports");
882                assert!(a.overrides.is_empty());
883            }
884            _ => panic!("expected Apply"),
885        }
886    }
887
888    #[test]
889    fn parse_color_map_block() {
890        let src = r##"
891            timeline "テスト" {
892                title "テスト";
893                unit year;
894                range 0..2000;
895                color_map {
896                    dynasty: "#3366cc";
897                    war: "#cc0000";
898                }
899            }
900        "##;
901        let file = parse(src).unwrap();
902        assert_eq!(file.statements.len(), 1);
903        match &file.statements[0].node {
904            ast::Statement::Timeline(t) => {
905                assert_eq!(t.color_map.len(), 2);
906                assert!(
907                    t.color_map
908                        .iter()
909                        .any(|(k, v)| k == "dynasty" && v == "#3366cc")
910                );
911                assert!(
912                    t.color_map
913                        .iter()
914                        .any(|(k, v)| k == "war" && v == "#cc0000")
915                );
916            }
917            _ => panic!("expected Timeline"),
918        }
919    }
920
921    #[test]
922    fn parse_color_map_block_empty() {
923        let src = r#"timeline "T" { color_map {} }"#;
924        let file = parse(src).unwrap();
925        match &file.statements[0].node {
926            ast::Statement::Timeline(t) => assert!(t.color_map.is_empty()),
927            _ => panic!("expected Timeline"),
928        }
929    }
930
931    #[test]
932    fn parse_five_digit_year_still_works() {
933        // 後方互換: bench / 既存テストで使われている 5 桁以上の年が引き続き year_lit でマッチすること
934        let src = r#"timeline "T" { unit year; range 0..10000; }"#;
935        let file = parse(src).unwrap();
936        match &file.statements[0].node {
937            ast::Statement::Timeline(t) => {
938                assert_eq!(
939                    t.range,
940                    Some(ast::RangeExpr {
941                        start: ast::TimeValue::Year(0),
942                        end: ast::TimeValue::Year(10000),
943                    })
944                );
945            }
946            _ => panic!("expected Timeline"),
947        }
948    }
949
950    #[test]
951    fn parse_six_digit_year_in_event() {
952        let src = r#"event ancient 100000 "未来" {};"#;
953        let file = parse(src).unwrap();
954        match &file.statements[0].node {
955            ast::Statement::Event(e) => {
956                assert_eq!(e.time, ast::TimeValue::Year(100000));
957            }
958            _ => panic!("expected Event"),
959        }
960    }
961
962    #[test]
963    fn test_field_priority_policy_parse() {
964        let src = r#"
965            import wikidata as wd {
966                entity Q123 as foo;
967                policy field_priority {
968                    label: manual;
969                    time: wikidata;
970                    tags: merge;
971                }
972            }
973        "#;
974        let result = parse(src);
975        assert!(result.is_ok(), "parse failed: {:?}", result.err());
976        let file = result.unwrap();
977        let import = match &file.statements[0].node {
978            crate::ast::Statement::Import(b) => b,
979            _ => panic!("expected import"),
980        };
981        match import.policy {
982            Some(crate::ast::ReimportPolicy::FieldPriority(config)) => {
983                assert_eq!(config.label, crate::ast::FieldStrategy::Manual);
984                assert_eq!(config.time, crate::ast::FieldStrategy::Wikidata);
985                assert_eq!(config.tags, crate::ast::FieldStrategy::Merge);
986            }
987            other => panic!("expected FieldPriority, got {:?}", other),
988        }
989    }
990
991    // ─── 日付リテラル (#243) ─────────────────────────────────────────
992
993    #[test]
994    fn parse_date_literal_event() {
995        let src = r#"event han 1969-07-20 "月面着陸" {};"#;
996        let file = parse(src).unwrap();
997        match &file.statements[0].node {
998            ast::Statement::Event(e) => {
999                assert_eq!(e.time, ast::TimeValue::Date(1969, 7, 20));
1000            }
1001            _ => panic!("expected Event"),
1002        }
1003    }
1004
1005    #[test]
1006    fn parse_year_month_literal_event() {
1007        let src = r#"event han 1969-07 "月着陸の月" {};"#;
1008        let file = parse(src).unwrap();
1009        match &file.statements[0].node {
1010            ast::Statement::Event(e) => {
1011                assert_eq!(e.time, ast::TimeValue::YearMonth(1969, 7));
1012            }
1013            _ => panic!("expected Event"),
1014        }
1015    }
1016
1017    #[test]
1018    fn parse_span_with_dates() {
1019        let src = r#"span ww2 1939-09-01..1945-09-02 "第二次世界大戦" {};"#;
1020        let file = parse(src).unwrap();
1021        match &file.statements[0].node {
1022            ast::Statement::Span(s) => {
1023                assert_eq!(s.start, ast::TimeValue::Date(1939, 9, 1));
1024                assert_eq!(s.end, ast::TimeValue::Date(1945, 9, 2));
1025            }
1026            _ => panic!("expected Span"),
1027        }
1028    }
1029
1030    #[test]
1031    fn parse_span_with_mixed_precision() {
1032        let src = r#"span partial 1900..1969-07-20 "混在範囲" {};"#;
1033        let file = parse(src).unwrap();
1034        match &file.statements[0].node {
1035            ast::Statement::Span(s) => {
1036                assert_eq!(s.start, ast::TimeValue::Year(1900));
1037                assert_eq!(s.end, ast::TimeValue::Date(1969, 7, 20));
1038            }
1039            _ => panic!("expected Span"),
1040        }
1041    }
1042
1043    #[test]
1044    fn parse_event_range_with_year_month() {
1045        let src = r#"event_range han 1939-09..1945-09 "第二次世界大戦" {};"#;
1046        let file = parse(src).unwrap();
1047        match &file.statements[0].node {
1048            ast::Statement::EventRange(er) => {
1049                assert_eq!(er.start, ast::TimeValue::YearMonth(1939, 9));
1050                assert_eq!(er.end, ast::TimeValue::YearMonth(1945, 9));
1051            }
1052            _ => panic!("expected EventRange"),
1053        }
1054    }
1055
1056    #[test]
1057    fn parse_range_directive_with_dates() {
1058        let src = r#"
1059            timeline "近代史" {
1060                unit month;
1061                range 1939-01..1946-01;
1062            }
1063        "#;
1064        let file = parse(src).unwrap();
1065        match &file.statements[0].node {
1066            ast::Statement::Timeline(t) => {
1067                assert_eq!(
1068                    t.range,
1069                    Some(ast::RangeExpr {
1070                        start: ast::TimeValue::YearMonth(1939, 1),
1071                        end: ast::TimeValue::YearMonth(1946, 1),
1072                    })
1073                );
1074            }
1075            _ => panic!("expected Timeline"),
1076        }
1077    }
1078
1079    #[test]
1080    fn parse_negative_year_still_works() {
1081        let src = r#"event qin -206 "始皇帝即位" {};"#;
1082        let file = parse(src).unwrap();
1083        match &file.statements[0].node {
1084            ast::Statement::Event(e) => {
1085                assert_eq!(e.time, ast::TimeValue::Year(-206));
1086            }
1087            _ => panic!("expected Event"),
1088        }
1089    }
1090
1091    #[test]
1092    fn parse_invalid_month_zero_fails() {
1093        let src = r#"event han 1969-00-20 "invalid" {};"#;
1094        let result = parse(src);
1095        assert!(result.is_err(), "expected error for month=00");
1096        let msg = format!("{}", result.unwrap_err());
1097        assert!(
1098            msg.contains("Invalid month") || msg.to_lowercase().contains("month"),
1099            "unexpected error: {msg}"
1100        );
1101    }
1102
1103    #[test]
1104    fn parse_invalid_month_thirteen_fails() {
1105        let src = r#"event han 1969-13-20 "invalid" {};"#;
1106        let result = parse(src);
1107        assert!(result.is_err(), "expected error for month=13");
1108    }
1109
1110    #[test]
1111    fn parse_invalid_day_zero_fails() {
1112        let src = r#"event han 1969-07-00 "invalid" {};"#;
1113        let result = parse(src);
1114        assert!(result.is_err(), "expected error for day=00");
1115    }
1116
1117    #[test]
1118    fn parse_invalid_day_thirtytwo_fails() {
1119        let src = r#"event han 1969-07-32 "invalid" {};"#;
1120        let result = parse(src);
1121        assert!(result.is_err(), "expected error for day=32");
1122    }
1123
1124    #[test]
1125    fn parse_negative_year_with_month_rejected() {
1126        // 紀元前は year 精度のみ。仕様書 §1.3 に従い year_month/date は符号なし。
1127        let src = r#"event ancient -206-01 "鴻門の会" {};"#;
1128        let result = parse(src);
1129        assert!(result.is_err(), "expected error for negative YearMonth");
1130    }
1131
1132    #[test]
1133    fn parse_time_value_display_round_trip() {
1134        // Display 実装が `Date` を `YYYY-MM-DD` で表示することを確認
1135        let d = ast::TimeValue::Date(1969, 7, 20);
1136        assert_eq!(format!("{d}"), "1969-07-20");
1137        let m = ast::TimeValue::YearMonth(1939, 9);
1138        assert_eq!(format!("{m}"), "1939-09");
1139        let y = ast::TimeValue::Year(-206);
1140        assert_eq!(format!("{y}"), "-206");
1141    }
1142
1143    #[test]
1144    fn parse_time_value_accessors() {
1145        let d = ast::TimeValue::Date(1969, 7, 20);
1146        assert_eq!(d.year(), 1969);
1147        assert_eq!(d.month(), Some(7));
1148        assert_eq!(d.day(), Some(20));
1149
1150        let m = ast::TimeValue::YearMonth(1939, 9);
1151        assert_eq!(m.year(), 1939);
1152        assert_eq!(m.month(), Some(9));
1153        assert_eq!(m.day(), None);
1154
1155        let y = ast::TimeValue::Year(-206);
1156        assert_eq!(y.year(), -206);
1157        assert_eq!(y.month(), None);
1158        assert_eq!(y.day(), None);
1159    }
1160
1161    #[test]
1162    fn parse_time_value_to_sortable_order() {
1163        use ast::TimeValue::*;
1164        assert!(Year(1939).to_sortable() < Year(1940).to_sortable());
1165        assert!(YearMonth(1939, 9).to_sortable() > YearMonth(1939, 8).to_sortable());
1166        assert!(Date(1939, 9, 1).to_sortable() > Date(1939, 8, 31).to_sortable());
1167        // 同年の Year は (year, 0, 0) なので、月日付きより前に位置する
1168        assert!(Year(1939).to_sortable() < YearMonth(1939, 1).to_sortable());
1169    }
1170
1171    // ─── parse_time_literal (公開 API) ───────────────────────
1172
1173    #[test]
1174    fn parse_time_literal_year() {
1175        assert_eq!(
1176            parse_time_literal("2020").unwrap(),
1177            ast::TimeValue::Year(2020)
1178        );
1179        assert_eq!(
1180            parse_time_literal("-206").unwrap(),
1181            ast::TimeValue::Year(-206)
1182        );
1183        assert_eq!(parse_time_literal("0").unwrap(), ast::TimeValue::Year(0));
1184    }
1185
1186    #[test]
1187    fn parse_time_literal_year_month() {
1188        assert_eq!(
1189            parse_time_literal("1939-09").unwrap(),
1190            ast::TimeValue::YearMonth(1939, 9)
1191        );
1192    }
1193
1194    #[test]
1195    fn parse_time_literal_date() {
1196        assert_eq!(
1197            parse_time_literal("1969-07-20").unwrap(),
1198            ast::TimeValue::Date(1969, 7, 20)
1199        );
1200    }
1201
1202    #[test]
1203    fn parse_time_literal_invalid_month_rejected() {
1204        assert!(parse_time_literal("2020-13").is_err());
1205        assert!(parse_time_literal("2020-00").is_err());
1206    }
1207
1208    #[test]
1209    fn parse_time_literal_invalid_day_rejected() {
1210        assert!(parse_time_literal("2020-02-32").is_err());
1211        assert!(parse_time_literal("2020-02-00").is_err());
1212    }
1213
1214    #[test]
1215    fn parse_time_literal_trailing_garbage_rejected() {
1216        // EOI 制約により末尾の非空白ゴミは拒否される
1217        assert!(parse_time_literal("1969-07-20foo").is_err());
1218        assert!(parse_time_literal("2020 extra").is_err());
1219    }
1220
1221    #[test]
1222    fn parse_time_literal_strips_outer_whitespace() {
1223        // CSV パディング想定: 前後の空白は trim して解釈する
1224        assert_eq!(
1225            parse_time_literal("  2020  ").unwrap(),
1226            ast::TimeValue::Year(2020)
1227        );
1228        assert_eq!(
1229            parse_time_literal("\t1969-07-20\n").unwrap(),
1230            ast::TimeValue::Date(1969, 7, 20)
1231        );
1232    }
1233
1234    #[test]
1235    fn parse_time_literal_non_numeric_rejected() {
1236        assert!(parse_time_literal("abc").is_err());
1237        assert!(parse_time_literal("").is_err());
1238    }
1239
1240    #[test]
1241    fn parse_time_literal_negative_with_month_rejected() {
1242        // 紀元前は year 精度のみ。仕様書 §1.3 と整合させる
1243        assert!(parse_time_literal("-206-01").is_err());
1244    }
1245
1246    // ─── 時間式オフセット (#148) ──────────────────────────────────
1247
1248    #[test]
1249    fn parse_claim_expr_with_positive_offset() {
1250        let src = r#"
1251            map wd.x to span {
1252                lane a;
1253                start claim(P569).year +1;
1254                end claim(P570).year +30;
1255                label label@ja;
1256            }
1257        "#;
1258        let file = parse(src).expect("should parse claim_expr with positive offset");
1259        match &file.statements[0].node {
1260            ast::Statement::Map(m) => {
1261                let start = m
1262                    .props
1263                    .iter()
1264                    .find_map(|p| match p {
1265                        ast::MapProp::Start(e) => Some(e),
1266                        _ => None,
1267                    })
1268                    .expect("start present");
1269                assert_eq!(start.fallbacks.len(), 1);
1270                match &start.fallbacks[0] {
1271                    ast::MapFallback::Claim(c) => {
1272                        assert_eq!(c.claim.property, "P569");
1273                        assert_eq!(c.accessor.as_deref(), Some("year"));
1274                        assert_eq!(c.offset, Some(1));
1275                    }
1276                    _ => panic!("expected Claim"),
1277                }
1278
1279                let end = m
1280                    .props
1281                    .iter()
1282                    .find_map(|p| match p {
1283                        ast::MapProp::End(e) => Some(e),
1284                        _ => None,
1285                    })
1286                    .expect("end present");
1287                match &end.fallbacks[0] {
1288                    ast::MapFallback::Claim(c) => assert_eq!(c.offset, Some(30)),
1289                    _ => panic!("expected Claim"),
1290                }
1291            }
1292            _ => panic!("expected Map"),
1293        }
1294    }
1295
1296    #[test]
1297    fn parse_claim_expr_with_negative_offset() {
1298        let src = r#"
1299            map wd.x to span {
1300                lane a;
1301                start claim(P569).year -5;
1302                end claim(P570).year -100;
1303                label label@ja;
1304            }
1305        "#;
1306        let file = parse(src).expect("should parse claim_expr with negative offset");
1307        match &file.statements[0].node {
1308            ast::Statement::Map(m) => {
1309                let start = m
1310                    .props
1311                    .iter()
1312                    .find_map(|p| match p {
1313                        ast::MapProp::Start(e) => Some(e),
1314                        _ => None,
1315                    })
1316                    .expect("start present");
1317                match &start.fallbacks[0] {
1318                    ast::MapFallback::Claim(c) => assert_eq!(c.offset, Some(-5)),
1319                    _ => panic!("expected Claim"),
1320                }
1321
1322                let end = m
1323                    .props
1324                    .iter()
1325                    .find_map(|p| match p {
1326                        ast::MapProp::End(e) => Some(e),
1327                        _ => None,
1328                    })
1329                    .expect("end present");
1330                match &end.fallbacks[0] {
1331                    ast::MapFallback::Claim(c) => assert_eq!(c.offset, Some(-100)),
1332                    _ => panic!("expected Claim"),
1333                }
1334            }
1335            _ => panic!("expected Map"),
1336        }
1337    }
1338
1339    #[test]
1340    fn parse_claim_expr_without_offset_is_none() {
1341        let src = r#"
1342            map wd.x to span {
1343                lane a;
1344                start claim(P569).year;
1345                end claim(P570).year;
1346                label label@ja;
1347            }
1348        "#;
1349        let file = parse(src).expect("should parse claim_expr without offset");
1350        match &file.statements[0].node {
1351            ast::Statement::Map(m) => {
1352                let start = m
1353                    .props
1354                    .iter()
1355                    .find_map(|p| match p {
1356                        ast::MapProp::Start(e) => Some(e),
1357                        _ => None,
1358                    })
1359                    .expect("start present");
1360                match &start.fallbacks[0] {
1361                    ast::MapFallback::Claim(c) => assert_eq!(c.offset, None),
1362                    _ => panic!("expected Claim"),
1363                }
1364            }
1365            _ => panic!("expected Map"),
1366        }
1367    }
1368
1369    #[test]
1370    fn parse_claim_expr_offset_with_fallback() {
1371        // フォールバックチェーン `??` 内でもオフセットが正しくパースされる
1372        let src = r#"
1373            map wd.x to span {
1374                lane a;
1375                start claim(P580).year +1 ?? claim(P571).year -10;
1376                end claim(P582).year;
1377                label label@ja;
1378            }
1379        "#;
1380        let file = parse(src).expect("should parse claim_expr with offset in fallback chain");
1381        match &file.statements[0].node {
1382            ast::Statement::Map(m) => {
1383                let start = m
1384                    .props
1385                    .iter()
1386                    .find_map(|p| match p {
1387                        ast::MapProp::Start(e) => Some(e),
1388                        _ => None,
1389                    })
1390                    .expect("start present");
1391                assert_eq!(start.fallbacks.len(), 2);
1392                match &start.fallbacks[0] {
1393                    ast::MapFallback::Claim(c) => {
1394                        assert_eq!(c.claim.property, "P580");
1395                        assert_eq!(c.offset, Some(1));
1396                    }
1397                    _ => panic!("expected Claim"),
1398                }
1399                match &start.fallbacks[1] {
1400                    ast::MapFallback::Claim(c) => {
1401                        assert_eq!(c.claim.property, "P571");
1402                        assert_eq!(c.offset, Some(-10));
1403                    }
1404                    _ => panic!("expected Claim"),
1405                }
1406            }
1407            _ => panic!("expected Map"),
1408        }
1409    }
1410
1411    #[test]
1412    fn parse_map_expr_literal_fallback() {
1413        // claim(P569).year ?? 9999 のようなリテラルフォールバックが正しくパースされる
1414        let src = r#"
1415            map wd.x to span {
1416                lane a;
1417                start claim(P569).year ?? 9999;
1418                end claim(P570).year ?? -999;
1419                label label@ja;
1420            }
1421        "#;
1422        let file = parse(src).unwrap();
1423        match &file.statements[0].node {
1424            ast::Statement::Map(m) => {
1425                let start = m
1426                    .props
1427                    .iter()
1428                    .find_map(|p| match p {
1429                        ast::MapProp::Start(e) => Some(e),
1430                        _ => None,
1431                    })
1432                    .expect("start present");
1433                assert_eq!(start.fallbacks.len(), 2);
1434                match &start.fallbacks[0] {
1435                    ast::MapFallback::Claim(c) => {
1436                        assert_eq!(c.claim.property, "P569");
1437                        assert_eq!(c.accessor.as_deref(), Some("year"));
1438                    }
1439                    _ => panic!("expected Claim"),
1440                }
1441                match &start.fallbacks[1] {
1442                    ast::MapFallback::Literal(n) => assert_eq!(*n, 9999),
1443                    _ => panic!("expected Literal"),
1444                }
1445
1446                let end = m
1447                    .props
1448                    .iter()
1449                    .find_map(|p| match p {
1450                        ast::MapProp::End(e) => Some(e),
1451                        _ => None,
1452                    })
1453                    .expect("end present");
1454                match &end.fallbacks[1] {
1455                    ast::MapFallback::Literal(n) => assert_eq!(*n, -999),
1456                    _ => panic!("expected Literal"),
1457                }
1458            }
1459            _ => panic!("expected Map"),
1460        }
1461    }
1462
1463    #[test]
1464    fn parse_map_expr_claim_claim_literal_fallback_chain() {
1465        // claim ?? claim ?? literal の 3 段フォールバック
1466        let src = r#"
1467            map wd.x to event {
1468                lane a;
1469                time claim(P580).year ?? claim(P571).year ?? 0;
1470                label label@ja;
1471            }
1472        "#;
1473        let file = parse(src).unwrap();
1474        match &file.statements[0].node {
1475            ast::Statement::Map(m) => {
1476                let time = m
1477                    .props
1478                    .iter()
1479                    .find_map(|p| match p {
1480                        ast::MapProp::Time(e) => Some(e),
1481                        _ => None,
1482                    })
1483                    .expect("time present");
1484                assert_eq!(time.fallbacks.len(), 3);
1485                match &time.fallbacks[2] {
1486                    ast::MapFallback::Literal(n) => assert_eq!(*n, 0),
1487                    _ => panic!("expected Literal"),
1488                }
1489            }
1490            _ => panic!("expected Map"),
1491        }
1492    }
1493
1494    #[test]
1495    fn test_field_priority_partial_fields() {
1496        // デフォルト値が適用されること
1497        let src = r#"
1498            import wikidata as wd {
1499                entity Q123 as foo;
1500                policy field_priority {
1501                    tags: wikidata;
1502                }
1503            }
1504        "#;
1505        let result = parse(src);
1506        assert!(result.is_ok());
1507        let file = result.unwrap();
1508        let import = match &file.statements[0].node {
1509            crate::ast::Statement::Import(b) => b,
1510            _ => panic!("expected import"),
1511        };
1512        match import.policy {
1513            Some(crate::ast::ReimportPolicy::FieldPriority(config)) => {
1514                assert_eq!(config.label, crate::ast::FieldStrategy::Manual); // default
1515                assert_eq!(config.time, crate::ast::FieldStrategy::Wikidata); // default
1516                assert_eq!(config.tags, crate::ast::FieldStrategy::Wikidata); // overridden
1517            }
1518            other => panic!("expected FieldPriority, got {:?}", other),
1519        }
1520    }
1521
1522    // ─── source_location テスト ───────────────────────────────────────────
1523
1524    /// pest Syntax エラーから line/col が正しく取れること(1-based)。
1525    #[test]
1526    fn source_location_syntax_error_pos() {
1527        // 1行目に不正なトークン
1528        let src = "@@@";
1529        let err = parse(src).unwrap_err();
1530        let loc = err.source_location(src).expect("should have location");
1531        assert_eq!(loc.line, 1, "1行目のエラーは line=1");
1532        assert!(loc.col >= 1, "col は 1-based で 1 以上");
1533    }
1534
1535    /// pest Syntax エラーが複数行テキストの 2 行目にある場合。
1536    #[test]
1537    fn source_location_syntax_error_second_line() {
1538        let src = "// comment\n@@@";
1539        let err = parse(src).unwrap_err();
1540        let loc = err.source_location(src).expect("should have location");
1541        // エラーは 2行目(1-based)
1542        assert_eq!(loc.line, 2, "2行目のエラーは line=2");
1543    }
1544
1545    /// InvalidMonth variant でバイトオフセットから line/col に変換できること。
1546    #[test]
1547    fn source_location_invalid_month_byte_offset() {
1548        // 月が 13 の不正な日付を作る: "2023-13" は grammar では year+month で
1549        // month の範囲外(13)が builder で InvalidMonth エラーになる
1550        let src = r#"
1551timeline "t" {
1552    title "t";
1553    unit year;
1554    range 2023-01..2023-13;
1555    calendar proleptic_gregorian;
1556}
1557"#;
1558        // InvalidMonth エラーが発生するはず
1559        match parse(src) {
1560            Err(e @ error::ParseError::InvalidMonth { .. }) => {
1561                let loc = e.source_location(src);
1562                // 位置情報が取れれば OK(値の正確さより変換が panic しないことを確認)
1563                assert!(loc.is_some(), "InvalidMonth は source_location を返す");
1564                let loc = loc.unwrap();
1565                assert!(loc.line >= 1, "line は 1-based で 1 以上");
1566                assert!(loc.col >= 1, "col は 1-based で 1 以上");
1567            }
1568            // grammar によっては Syntax エラーになることもある(その場合もテスト通過)
1569            Err(e @ error::ParseError::Syntax(_)) => {
1570                let loc = e.source_location(src);
1571                assert!(loc.is_some(), "Syntax error も source_location を返す");
1572            }
1573            Ok(_) => {
1574                // grammar が月境界チェックを別途行う場合はスキップ
1575            }
1576            Err(other) => panic!("unexpected error: {:?}", other),
1577        }
1578    }
1579
1580    /// UnknownPolicy は位置情報を持たない(None を返す)。
1581    #[test]
1582    fn source_location_unknown_policy_returns_none() {
1583        let err = error::ParseError::UnknownPolicy("bogus".to_string());
1584        assert!(
1585            err.source_location("anything").is_none(),
1586            "UnknownPolicy は source_location = None"
1587        );
1588    }
1589
1590    // ─── group ブロック ───────────────────────────────────────────
1591
1592    #[test]
1593    fn parse_group_with_two_lanes() {
1594        let src = r#"
1595            group "古代" {
1596                lane "秦" as qin {}
1597                lane "漢" as han { kind dynasty; order 10; }
1598            }
1599        "#;
1600        let file = parse(src).expect("group with two lanes must parse");
1601        assert_eq!(file.statements.len(), 1);
1602        match &file.statements[0].node {
1603            ast::Statement::Group(g) => {
1604                assert_eq!(g.label, "古代");
1605                assert_eq!(g.lanes.len(), 2);
1606                assert_eq!(g.lanes[0].label, "秦");
1607                assert_eq!(g.lanes[0].alias.as_deref(), Some("qin"));
1608                assert_eq!(g.lanes[1].label, "漢");
1609                assert_eq!(g.lanes[1].kind.as_deref(), Some("dynasty"));
1610            }
1611            other => panic!("expected Group, got {other:?}"),
1612        }
1613    }
1614
1615    #[test]
1616    fn parse_group_single_lane() {
1617        let src = r#"group "単一" { lane "A" as a {} }"#;
1618        let file = parse(src).expect("group with single lane must parse");
1619        match &file.statements[0].node {
1620            ast::Statement::Group(g) => {
1621                assert_eq!(g.label, "単一");
1622                assert_eq!(g.lanes.len(), 1);
1623            }
1624            other => panic!("expected Group, got {other:?}"),
1625        }
1626    }
1627
1628    #[test]
1629    fn parse_group_mixed_with_other_statements() {
1630        let src = r#"
1631            lane "独立" as standalone {}
1632            group "グループ" {
1633                lane "子A" as child_a {}
1634            }
1635            lane "独立2" as standalone2 {}
1636        "#;
1637        let file = parse(src).expect("group mixed with lanes must parse");
1638        assert_eq!(file.statements.len(), 3);
1639        assert!(matches!(file.statements[0].node, ast::Statement::Lane(_)));
1640        assert!(matches!(file.statements[1].node, ast::Statement::Group(_)));
1641        assert!(matches!(file.statements[2].node, ast::Statement::Lane(_)));
1642    }
1643
1644    #[test]
1645    fn parse_group_without_lanes_fails() {
1646        // group ブロックには lane が 1 つ以上必要(grammar: lane_decl+)
1647        let src = r#"group "空" {}"#;
1648        assert!(
1649            parse(src).is_err(),
1650            "group without any lane must fail to parse"
1651        );
1652    }
1653
1654    // ─── ParseDiagnostic SourceSpan テスト (#355) ─────────────────────────────
1655
1656    /// 不正トークンの構文エラーで ParseDiagnostic が正しい SourceSpan を持つこと。
1657    #[test]
1658    fn parse_diagnostic_syntax_error_has_source_span() {
1659        let src = "@@@";
1660        let err = parse(src).unwrap_err();
1661        let diag = error::ParseDiagnostic::from_parse_error(&err, src, "test.tdsl");
1662        // span は Some であること(位置情報が付与される)
1663        assert!(
1664            diag.span().is_some(),
1665            "Syntax エラーは SourceSpan を持つべき"
1666        );
1667        let span = diag.span().unwrap();
1668        // オフセットはソース長以内
1669        assert!(
1670            span.offset() <= src.len(),
1671            "offset {} はソース長 {} 以内であるべき",
1672            span.offset(),
1673            src.len()
1674        );
1675        // 長さは 1 以上
1676        assert!(!span.is_empty(), "len は 1 以上であるべき");
1677    }
1678
1679    /// 複数行ソースの 2 行目にある構文エラーで offset が行頭バイトを含むこと。
1680    #[test]
1681    fn parse_diagnostic_multiline_second_line_span() {
1682        let src = "// comment\n@@@";
1683        let err = parse(src).unwrap_err();
1684        let diag = error::ParseDiagnostic::from_parse_error(&err, src, "test.tdsl");
1685        let span = diag.span().expect("span must be Some");
1686        // 2行目の先頭は offset 11("// comment\n" = 11 bytes)
1687        assert!(
1688            span.offset() >= 11,
1689            "2 行目のエラーは offset >= 11 であるべき(実際: {})",
1690            span.offset()
1691        );
1692    }
1693
1694    /// 位置情報のない UnknownPolicy は span が None であること。
1695    #[test]
1696    fn parse_diagnostic_unknown_policy_no_span() {
1697        let err = error::ParseError::UnknownPolicy("bogus".to_string());
1698        let diag = error::ParseDiagnostic::from_parse_error(&err, "anything", "test.tdsl");
1699        assert!(
1700            diag.span().is_none(),
1701            "UnknownPolicy は SourceSpan を持たないべき"
1702        );
1703    }
1704
1705    /// バイトオフセット variant(InvalidMonth)で span が構築できること。
1706    #[test]
1707    fn parse_diagnostic_invalid_month_has_span() {
1708        let src = r#"
1709timeline "t" {
1710    title "t";
1711    unit year;
1712    range 2023-01..2023-13;
1713    calendar proleptic_gregorian;
1714}
1715"#;
1716        match parse(src) {
1717            Err(ref e @ error::ParseError::InvalidMonth { .. }) => {
1718                let diag = error::ParseDiagnostic::from_parse_error(e, src, "test.tdsl");
1719                let span = diag.span();
1720                // span が Some であり panic しないこと
1721                assert!(span.is_some(), "InvalidMonth は SourceSpan を持つべき");
1722                let span = span.unwrap();
1723                assert!(span.offset() <= src.len());
1724                assert!(!span.is_empty());
1725            }
1726            Err(error::ParseError::Syntax(_)) => {
1727                // grammar の都合で Syntax エラーになる場合も通過させる
1728            }
1729            Ok(_) => {
1730                // grammar が月境界チェックを行わない場合はスキップ
1731            }
1732            Err(other) => panic!("unexpected error: {other:?}"),
1733        }
1734    }
1735
1736    // ─── qualifier アクセス (#361) ────────────────────────────────────────────
1737
1738    #[test]
1739    fn parse_claim_qualifier_access() {
1740        // claim(P39).qualifier(P580).year をパースできる
1741        let src = r#"
1742            timeline "test" {}
1743            import wd as w { entity Q1; }
1744            map wd.person to span {
1745                lane x;
1746                start claim(P39).qualifier(P580).year;
1747                end   claim(P39).qualifier(P582).year;
1748                label label@ja;
1749            }
1750        "#;
1751        let file = parse(src).unwrap();
1752        let map = match &file.statements[2].node {
1753            ast::Statement::Map(m) => m,
1754            _ => panic!("expected Map"),
1755        };
1756
1757        let start = map
1758            .props
1759            .iter()
1760            .find_map(|p| match p {
1761                ast::MapProp::Start(e) => Some(e),
1762                _ => None,
1763            })
1764            .expect("start present");
1765
1766        match &start.fallbacks[0] {
1767            ast::MapFallback::Claim(c) => {
1768                assert_eq!(c.claim.property, "P39");
1769                assert_eq!(c.qualifier.as_deref(), Some("P580"));
1770                assert_eq!(c.accessor.as_deref(), Some("year"));
1771                assert_eq!(c.offset, None);
1772            }
1773            _ => panic!("expected Claim"),
1774        }
1775
1776        let end = map
1777            .props
1778            .iter()
1779            .find_map(|p| match p {
1780                ast::MapProp::End(e) => Some(e),
1781                _ => None,
1782            })
1783            .expect("end present");
1784
1785        match &end.fallbacks[0] {
1786            ast::MapFallback::Claim(c) => {
1787                assert_eq!(c.claim.property, "P39");
1788                assert_eq!(c.qualifier.as_deref(), Some("P582"));
1789                assert_eq!(c.accessor.as_deref(), Some("year"));
1790            }
1791            _ => panic!("expected Claim"),
1792        }
1793    }
1794
1795    #[test]
1796    fn parse_map_expand() {
1797        // expand claim(P39); をパースできる
1798        let src = r#"
1799            timeline "test" {}
1800            import wd as w { entity Q1; }
1801            map wd.person to span {
1802                lane x;
1803                expand claim(P39);
1804                start claim(P39).qualifier(P580).year;
1805                end   claim(P39).qualifier(P582).year;
1806                label label@ja;
1807            }
1808        "#;
1809        let file = parse(src).unwrap();
1810        let map = match &file.statements[2].node {
1811            ast::Statement::Map(m) => m,
1812            _ => panic!("expected Map"),
1813        };
1814
1815        let expand = map
1816            .props
1817            .iter()
1818            .find_map(|p| match p {
1819                ast::MapProp::Expand(call) => Some(call),
1820                _ => None,
1821            })
1822            .expect("expand prop present");
1823
1824        assert_eq!(expand.property, "P39");
1825    }
1826
1827    #[test]
1828    fn parse_claim_without_qualifier_still_works() {
1829        // qualifier なし(従来の claim(P571).year)が引き続き動作する
1830        let src = r#"
1831            map wd.x to span {
1832                lane a;
1833                start claim(P571).year;
1834                end claim(P576).year;
1835                label label@ja;
1836            }
1837        "#;
1838        let file = parse(src).unwrap();
1839        match &file.statements[0].node {
1840            ast::Statement::Map(m) => {
1841                let start = m
1842                    .props
1843                    .iter()
1844                    .find_map(|p| match p {
1845                        ast::MapProp::Start(e) => Some(e),
1846                        _ => None,
1847                    })
1848                    .expect("start present");
1849                match &start.fallbacks[0] {
1850                    ast::MapFallback::Claim(c) => {
1851                        assert_eq!(c.claim.property, "P571");
1852                        assert_eq!(c.qualifier, None);
1853                        assert_eq!(c.accessor.as_deref(), Some("year"));
1854                    }
1855                    _ => panic!("expected Claim"),
1856                }
1857            }
1858            _ => panic!("expected Map"),
1859        }
1860    }
1861
1862    #[test]
1863    fn format_claim_qualifier_roundtrip() {
1864        // qualifier アクセスを含む DSL が format → reparse で同一 AST を返す
1865        let src = r#"map wd.person to span {
1866  lane x;
1867  expand claim(P39);
1868  start claim(P39).qualifier(P580).year;
1869  end claim(P39).qualifier(P582).year;
1870  label label@ja;
1871}
1872"#;
1873        let formatted = format::format_source(src).unwrap();
1874        assert_eq!(
1875            src, formatted,
1876            "format must be idempotent for qualifier syntax"
1877        );
1878    }
1879}