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
16pub fn parse(source: &str) -> Result<ast::File, error::ParseError> {
18 let pairs = TdslParser::parse(Rule::file, source)?;
19 builder::build_file(pairs)
20}
21
22pub 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 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 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 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 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 #[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 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 #[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 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 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 assert!(Year(1939).to_sortable() < YearMonth(1939, 1).to_sortable());
1169 }
1170
1171 #[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 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 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 assert!(parse_time_literal("-206-01").is_err());
1244 }
1245
1246 #[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 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 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 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 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); assert_eq!(config.time, crate::ast::FieldStrategy::Wikidata); assert_eq!(config.tags, crate::ast::FieldStrategy::Wikidata); }
1518 other => panic!("expected FieldPriority, got {:?}", other),
1519 }
1520 }
1521
1522 #[test]
1526 fn source_location_syntax_error_pos() {
1527 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 #[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 assert_eq!(loc.line, 2, "2行目のエラーは line=2");
1543 }
1544
1545 #[test]
1547 fn source_location_invalid_month_byte_offset() {
1548 let src = r#"
1551timeline "t" {
1552 title "t";
1553 unit year;
1554 range 2023-01..2023-13;
1555 calendar proleptic_gregorian;
1556}
1557"#;
1558 match parse(src) {
1560 Err(e @ error::ParseError::InvalidMonth { .. }) => {
1561 let loc = e.source_location(src);
1562 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 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 }
1576 Err(other) => panic!("unexpected error: {:?}", other),
1577 }
1578 }
1579
1580 #[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 #[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 let src = r#"group "空" {}"#;
1648 assert!(
1649 parse(src).is_err(),
1650 "group without any lane must fail to parse"
1651 );
1652 }
1653
1654 #[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 assert!(
1664 diag.span().is_some(),
1665 "Syntax エラーは SourceSpan を持つべき"
1666 );
1667 let span = diag.span().unwrap();
1668 assert!(
1670 span.offset() <= src.len(),
1671 "offset {} はソース長 {} 以内であるべき",
1672 span.offset(),
1673 src.len()
1674 );
1675 assert!(!span.is_empty(), "len は 1 以上であるべき");
1677 }
1678
1679 #[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 assert!(
1688 span.offset() >= 11,
1689 "2 行目のエラーは offset >= 11 であるべき(実際: {})",
1690 span.offset()
1691 );
1692 }
1693
1694 #[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 #[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 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 }
1729 Ok(_) => {
1730 }
1732 Err(other) => panic!("unexpected error: {other:?}"),
1733 }
1734 }
1735
1736 #[test]
1739 fn parse_claim_qualifier_access() {
1740 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 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 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 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}