1use std::collections::HashMap;
4
5use crate::kinds::{Kind, Number};
6
7use super::XetoError;
8use super::ast::{LibPragma, SlotDef, SpecDef, XetoFile};
9use super::lexer::{TokenType, XetoLexer};
10
11pub fn parse_xeto(source: &str) -> Result<XetoFile, XetoError> {
13 let mut lexer = XetoLexer::new(source);
14 let tokens = lexer.tokenize()?;
15 let mut parser = Parser::new(tokens);
16 parser.parse_file()
17}
18
19struct Parser {
24 tokens: Vec<super::lexer::Token>,
25 pos: usize,
26}
27
28impl Parser {
29 fn new(tokens: Vec<super::lexer::Token>) -> Self {
30 Self { tokens, pos: 0 }
31 }
32
33 fn peek(&self) -> &super::lexer::Token {
36 &self.tokens[self.pos]
37 }
38
39 fn peek_type(&self) -> &TokenType {
40 &self.tokens[self.pos].typ
41 }
42
43 fn at_end(&self) -> bool {
44 self.peek_type() == &TokenType::Eof
45 }
46
47 fn advance(&mut self) -> &super::lexer::Token {
48 let tok = &self.tokens[self.pos];
49 if tok.typ != TokenType::Eof {
50 self.pos += 1;
51 }
52 tok
53 }
54
55 fn expect(&mut self, typ: TokenType) -> Result<&super::lexer::Token, XetoError> {
56 let tok = &self.tokens[self.pos];
57 if tok.typ != typ {
58 return Err(XetoError::Parse {
59 line: tok.line,
60 col: tok.col,
61 message: format!("expected {:?}, got {:?} ('{}')", typ, tok.typ, tok.val),
62 });
63 }
64 Ok(self.advance())
65 }
66
67 fn skip_newlines(&mut self) {
68 while self.peek_type() == &TokenType::Newline {
69 self.advance();
70 }
71 }
72
73 fn collect_doc(&mut self) -> String {
76 let mut lines: Vec<String> = Vec::new();
77 loop {
78 if *self.peek_type() == TokenType::Comment {
80 let val = self.peek().val.clone();
81 self.advance();
82 let trimmed = val.trim();
84 if !trimmed.is_empty() && trimmed.chars().all(|c| c == '/') {
85 if *self.peek_type() == TokenType::Newline {
88 self.advance();
89 }
90 continue;
91 }
92 lines.push(val);
93 if *self.peek_type() == TokenType::Newline {
95 self.advance();
96 }
97 } else {
98 break;
99 }
100 }
101 lines.join("\n")
102 }
103
104 fn parse_file(&mut self) -> Result<XetoFile, XetoError> {
108 self.skip_newlines();
109
110 let pragma = if *self.peek_type() == TokenType::Ident && self.peek().val == "pragma" {
111 Some(self.parse_pragma()?)
112 } else {
113 None
114 };
115
116 let mut specs = Vec::new();
117 loop {
118 self.skip_newlines();
119 if self.at_end() {
120 break;
121 }
122
123 let doc = self.collect_doc();
125 self.skip_newlines();
126 if self.at_end() {
127 break;
128 }
129
130 let mut spec = self.parse_spec()?;
131 if !doc.is_empty() && spec.doc.is_empty() {
132 spec.doc = doc;
133 }
134 specs.push(spec);
135 }
136
137 Ok(XetoFile { pragma, specs })
138 }
139
140 fn parse_pragma(&mut self) -> Result<LibPragma, XetoError> {
142 self.expect(TokenType::Ident)?; self.expect(TokenType::Colon)?;
144 self.expect(TokenType::Ident)?; self.skip_newlines();
146
147 let meta = if *self.peek_type() == TokenType::LAngle {
148 self.parse_meta()?
149 } else {
150 HashMap::new()
151 };
152
153 let name = match meta.get("name") {
155 Some(Kind::Str(s)) => s.clone(),
156 _ => String::new(),
157 };
158 let version = match meta.get("version") {
159 Some(Kind::Str(s)) => s.clone(),
160 _ => String::new(),
161 };
162 let doc = match meta.get("doc") {
163 Some(Kind::Str(s)) => s.clone(),
164 _ => String::new(),
165 };
166
167 let depends = match meta.get("depends") {
169 Some(Kind::List(items)) => items
170 .iter()
171 .filter_map(|k| match k {
172 Kind::Str(s) => Some(s.clone()),
173 Kind::Dict(d) => {
174 if let Some(Kind::Str(lib_name)) = d.get("lib") {
175 Some(lib_name.clone())
176 } else {
177 None
178 }
179 }
180 _ => None,
181 })
182 .collect(),
183 Some(Kind::Dict(d)) => {
184 if let Some(Kind::Str(lib_name)) = d.get("lib") {
186 vec![lib_name.clone()]
187 } else {
188 Vec::new()
189 }
190 }
191 Some(Kind::Str(dep)) => vec![dep.clone()],
192 _ => Vec::new(),
193 };
194
195 Ok(LibPragma {
196 name,
197 version,
198 doc,
199 depends,
200 meta,
201 })
202 }
203
204 fn parse_spec(&mut self) -> Result<SpecDef, XetoError> {
206 let name = self.parse_dotted_name()?;
207 let mut spec = SpecDef::new(name);
208
209 self.skip_newlines();
210
211 if *self.peek_type() == TokenType::Colon {
213 self.advance();
214 self.skip_newlines();
215 if *self.peek_type() != TokenType::LAngle {
217 spec.base = Some(self.parse_type_ref()?);
218 self.skip_newlines();
219 }
220 }
221
222 if *self.peek_type() == TokenType::LAngle {
224 spec.meta = self.parse_meta()?;
225 self.skip_newlines();
226 }
227
228 if *self.peek_type() == TokenType::Str || *self.peek_type() == TokenType::Number {
230 spec.default = Some(self.parse_value()?);
231 self.skip_newlines();
232 }
233
234 if *self.peek_type() == TokenType::LBrace {
236 spec.slots = self.parse_body()?;
237 }
238
239 Ok(spec)
240 }
241
242 fn parse_body(&mut self) -> Result<Vec<SlotDef>, XetoError> {
244 self.expect(TokenType::LBrace)?;
245 self.skip_newlines();
246
247 let mut slots = Vec::new();
248 while *self.peek_type() != TokenType::RBrace && !self.at_end() {
249 let doc = self.collect_doc();
251 self.skip_newlines();
252
253 if *self.peek_type() == TokenType::RBrace {
254 break;
255 }
256
257 let mut slot = self.parse_slot()?;
258 if !doc.is_empty() && slot.doc.is_empty() {
259 slot.doc = doc;
260 }
261 slots.push(slot);
262 self.skip_newlines();
263 }
264
265 self.expect(TokenType::RBrace)?;
266 Ok(slots)
267 }
268
269 fn parse_slot(&mut self) -> Result<SlotDef, XetoError> {
273 let is_global = if *self.peek_type() == TokenType::Star {
274 self.advance();
275 self.skip_newlines();
276 true
277 } else {
278 false
279 };
280
281 let name = self.parse_dotted_name()?;
282 let mut slot = SlotDef::new(name);
283 slot.is_global = is_global;
284
285 self.skip_newlines();
286
287 if *self.peek_type() == TokenType::Colon {
289 self.advance();
290 self.skip_newlines();
291
292 let type_ref = self.parse_type_ref()?;
293
294 if type_ref == "Query" {
296 slot.is_query = true;
297 if *self.peek_type() == TokenType::LAngle {
299 let query_meta = self.parse_meta()?;
300 if let Some(Kind::Str(s)) = query_meta.get("of") {
301 slot.query_of = Some(s.clone());
302 }
303 if slot.query_of.is_none()
305 && let Some(Kind::Marker) = query_meta.get("of")
306 {
307 }
311 if let Some(Kind::Str(s)) = query_meta.get("via") {
312 slot.query_via = Some(s.clone());
313 }
314 if let Some(Kind::Str(s)) = query_meta.get("inverse") {
315 slot.query_inverse = Some(s.clone());
316 }
317 slot.meta = query_meta;
318 }
319 } else {
320 slot.type_ref = Some(type_ref);
321 }
322
323 self.skip_newlines();
325 if *self.peek_type() == TokenType::Question {
326 self.advance();
327 slot.is_maybe = true;
328 slot.meta.insert("maybe".to_string(), Kind::Marker);
329 }
330
331 self.skip_newlines();
333 if *self.peek_type() == TokenType::LAngle {
334 let extra_meta = self.parse_meta()?;
335 for (k, v) in extra_meta {
336 slot.meta.insert(k, v);
337 }
338 }
339
340 self.skip_newlines();
342 if !slot.is_maybe && *self.peek_type() == TokenType::Question {
343 self.advance();
344 slot.is_maybe = true;
345 slot.meta.insert("maybe".to_string(), Kind::Marker);
346 }
347
348 self.skip_newlines();
350 if *self.peek_type() == TokenType::LBrace {
351 slot.children = self.parse_body()?;
352 }
353
354 self.skip_newlines();
356 if *self.peek_type() == TokenType::Str || *self.peek_type() == TokenType::Number {
357 slot.default = Some(self.parse_value()?);
358 }
359 } else {
360 slot.is_marker = true;
362
363 if *self.peek_type() == TokenType::Question {
365 self.advance();
366 slot.is_maybe = true;
367 slot.meta.insert("maybe".to_string(), Kind::Marker);
368 }
369 }
370
371 Ok(slot)
372 }
373
374 fn parse_type_ref(&mut self) -> Result<String, XetoError> {
378 let mut name = String::new();
379
380 let tok = self.expect(TokenType::Ident)?;
382 name.push_str(&tok.val.clone());
383
384 while *self.peek_type() == TokenType::Dot {
386 self.advance();
387 let part = self.expect(TokenType::Ident)?;
388 name.push('.');
389 name.push_str(&part.val.clone());
390 }
391
392 if *self.peek_type() == TokenType::ColonColon {
394 self.advance();
395 let part = self.expect(TokenType::Ident)?;
396 name.push_str("::");
397 name.push_str(&part.val.clone());
398 }
399
400 Ok(name)
401 }
402
403 fn parse_meta(&mut self) -> Result<HashMap<String, Kind>, XetoError> {
405 self.expect(TokenType::LAngle)?;
406 self.skip_newlines();
407
408 let mut meta = HashMap::new();
409
410 while *self.peek_type() != TokenType::RAngle && !self.at_end() {
411 self.skip_newlines();
412 if *self.peek_type() == TokenType::RAngle {
413 break;
414 }
415
416 let tag_name = self.expect(TokenType::Ident)?;
417 let tag_name = tag_name.val.clone();
418 self.skip_newlines();
419
420 if *self.peek_type() == TokenType::Colon {
421 self.advance();
422 self.skip_newlines();
423 let value = self.parse_meta_value()?;
424 meta.insert(tag_name, value);
425 } else {
426 meta.insert(tag_name, Kind::Marker);
428 }
429
430 self.skip_newlines();
431
432 if *self.peek_type() == TokenType::Comma {
434 self.advance();
435 self.skip_newlines();
436 }
437 }
438
439 self.expect(TokenType::RAngle)?;
440 Ok(meta)
441 }
442
443 fn parse_meta_value(&mut self) -> Result<Kind, XetoError> {
445 match self.peek_type().clone() {
446 TokenType::Str => {
447 let tok = self.advance();
448 Ok(Kind::Str(tok.val.clone()))
449 }
450 TokenType::Number => {
451 let tok = self.advance();
452 let val = tok.val.clone();
453 Self::parse_number_val(&val)
454 }
455 TokenType::Ident => {
456 let tok = self.advance();
457 let val = tok.val.clone();
458 let mut full_name = val;
460 while *self.peek_type() == TokenType::Dot {
461 self.advance();
462 let part = self.expect(TokenType::Ident)?;
463 full_name.push('.');
464 full_name.push_str(&part.val.clone());
465 }
466 if *self.peek_type() == TokenType::ColonColon {
468 self.advance();
469 let part = self.expect(TokenType::Ident)?;
470 full_name.push_str("::");
471 full_name.push_str(&part.val.clone());
472 }
473 if *self.peek_type() == TokenType::LAngle {
475 let inner_meta = self.parse_meta()?;
476 let parts: Vec<String> = inner_meta
477 .iter()
478 .map(|(k, v)| format!("{}:{}", k, v))
479 .collect();
480 Ok(Kind::Str(format!("{}<{}>", full_name, parts.join(","))))
481 } else {
482 Ok(Kind::Str(full_name))
483 }
484 }
485 TokenType::LBrace => {
486 self.advance(); self.skip_newlines();
489 let mut items: Vec<Kind> = Vec::new();
490 while *self.peek_type() != TokenType::RBrace && *self.peek_type() != TokenType::Eof
491 {
492 if *self.peek_type() == TokenType::LBrace {
493 self.advance(); self.skip_newlines();
496 let mut dict = crate::data::HDict::new();
497 while *self.peek_type() != TokenType::RBrace
498 && *self.peek_type() != TokenType::Eof
499 {
500 let key = self.expect(TokenType::Ident)?.val.clone();
501 self.expect(TokenType::Colon)?;
502 self.skip_newlines();
503 let val = self.parse_meta_value()?;
504 dict.set(&key, val);
505 self.skip_newlines();
506 if *self.peek_type() == TokenType::Comma {
507 self.advance();
508 self.skip_newlines();
509 }
510 }
511 self.expect(TokenType::RBrace)?;
512 items.push(Kind::Dict(Box::new(dict)));
513 self.skip_newlines();
514 } else {
515 let _key = self.expect(TokenType::Ident)?.val.clone();
517 self.expect(TokenType::Colon)?;
518 self.skip_newlines();
519 let val = self.parse_meta_value()?;
520 items.push(val);
521 self.skip_newlines();
522 if *self.peek_type() == TokenType::Comma {
523 self.advance();
524 self.skip_newlines();
525 }
526 }
527 }
528 self.expect(TokenType::RBrace)?;
529 if items.len() == 1 {
530 Ok(items.into_iter().next().unwrap())
531 } else {
532 Ok(Kind::List(items))
533 }
534 }
535 _ => {
536 let tok = self.peek();
537 Err(XetoError::Parse {
538 line: tok.line,
539 col: tok.col,
540 message: format!("expected meta value, got {:?}", tok.typ),
541 })
542 }
543 }
544 }
545
546 fn parse_value(&mut self) -> Result<Kind, XetoError> {
548 match self.peek_type().clone() {
549 TokenType::Str => {
550 let tok = self.advance();
551 Ok(Kind::Str(tok.val.clone()))
552 }
553 TokenType::Number => {
554 let tok = self.advance();
555 Self::parse_number_val(&tok.val.clone())
556 }
557 _ => {
558 let tok = self.peek();
559 Err(XetoError::Parse {
560 line: tok.line,
561 col: tok.col,
562 message: format!("expected value literal, got {:?}", tok.typ),
563 })
564 }
565 }
566 }
567
568 fn parse_dotted_name(&mut self) -> Result<String, XetoError> {
570 let tok = self.expect(TokenType::Ident)?;
571 let mut name = tok.val.clone();
572
573 while *self.peek_type() == TokenType::Dot {
574 self.advance();
575 let part = self.expect(TokenType::Ident)?;
576 name.push('.');
577 name.push_str(&part.val.clone());
578 }
579
580 if *self.peek_type() == TokenType::ColonColon {
582 self.advance();
583 let part = self.expect(TokenType::Ident)?;
584 name.push_str("::");
585 name.push_str(&part.val.clone());
586 }
587
588 Ok(name)
589 }
590
591 fn parse_number_val(text: &str) -> Result<Kind, XetoError> {
593 let bytes = text.as_bytes();
597 let mut i = 0;
598 if i < bytes.len() && bytes[i] == b'-' {
600 i += 1;
601 }
602 while i < bytes.len() && bytes[i].is_ascii_digit() {
604 i += 1;
605 }
606 if i < bytes.len() && bytes[i] == b'.' {
608 i += 1;
609 while i < bytes.len() && bytes[i].is_ascii_digit() {
610 i += 1;
611 }
612 }
613 if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
615 i += 1;
616 if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
617 i += 1;
618 }
619 while i < bytes.len() && bytes[i].is_ascii_digit() {
620 i += 1;
621 }
622 }
623 let (num_str, unit_str) = text.split_at(i);
624 let val: f64 = num_str.parse().map_err(|_| XetoError::Parse {
625 line: 0,
626 col: 0,
627 message: format!("invalid number: {}", text),
628 })?;
629 let unit = if unit_str.is_empty() {
630 None
631 } else {
632 Some(unit_str.to_string())
633 };
634 Ok(Kind::Number(Number::new(val, unit)))
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn parse_empty_file() {
644 let file = parse_xeto("").unwrap();
645 assert!(file.pragma.is_none());
646 assert!(file.specs.is_empty());
647 }
648
649 #[test]
650 fn parse_simple_spec() {
651 let file = parse_xeto("Ahu : Equip {\n discharge\n return\n}").unwrap();
652 assert_eq!(file.specs.len(), 1);
653 let spec = &file.specs[0];
654 assert_eq!(spec.name, "Ahu");
655 assert_eq!(spec.base.as_deref(), Some("Equip"));
656 assert_eq!(spec.slots.len(), 2);
657 assert_eq!(spec.slots[0].name, "discharge");
658 assert!(spec.slots[0].is_marker);
659 assert_eq!(spec.slots[1].name, "return");
660 assert!(spec.slots[1].is_marker);
661 }
662
663 #[test]
664 fn parse_spec_with_meta() {
665 let file = parse_xeto("Ahu : Equip <abstract> {\n discharge\n}").unwrap();
666 let spec = &file.specs[0];
667 assert!(spec.meta.contains_key("abstract"));
668 assert_eq!(spec.meta.get("abstract"), Some(&Kind::Marker));
669 }
670
671 #[test]
672 fn parse_typed_slots() {
673 let file = parse_xeto("Site : Entity {\n dis : Str\n area : Number\n}").unwrap();
674 let spec = &file.specs[0];
675 assert_eq!(spec.slots.len(), 2);
676 assert_eq!(spec.slots[0].name, "dis");
677 assert_eq!(spec.slots[0].type_ref.as_deref(), Some("Str"));
678 assert!(!spec.slots[0].is_marker);
679 assert_eq!(spec.slots[1].name, "area");
680 assert_eq!(spec.slots[1].type_ref.as_deref(), Some("Number"));
681 }
682
683 #[test]
684 fn parse_marker_slots() {
685 let file = parse_xeto("Ahu : Equip {\n hot\n cold\n}").unwrap();
686 let spec = &file.specs[0];
687 assert!(spec.slots[0].is_marker);
688 assert!(spec.slots[1].is_marker);
689 }
690
691 #[test]
692 fn parse_maybe_slots() {
693 let file = parse_xeto("Foo : Bar {\n name : Str?\n}").unwrap();
694 let slot = &file.specs[0].slots[0];
695 assert_eq!(slot.name, "name");
696 assert_eq!(slot.type_ref.as_deref(), Some("Str"));
697 assert!(slot.is_maybe);
698 assert!(slot.meta.contains_key("maybe"));
699 }
700
701 #[test]
702 fn parse_query_slots() {
703 let file = parse_xeto("Ahu : Equip {\n points : Query<of:\"Point\", via:\"equipRef\">\n}")
704 .unwrap();
705 let slot = &file.specs[0].slots[0];
706 assert_eq!(slot.name, "points");
707 assert!(slot.is_query);
708 assert_eq!(slot.query_of.as_deref(), Some("Point"));
709 assert_eq!(slot.query_via.as_deref(), Some("equipRef"));
710 }
711
712 #[test]
713 fn parse_pragma() {
714 let src = r#"pragma : Lib <
715 name: "acme",
716 version: "1.0.0",
717 doc: "My lib"
718>
719Foo : Bar
720"#;
721 let file = parse_xeto(src).unwrap();
722 let pragma = file.pragma.as_ref().unwrap();
723 assert_eq!(pragma.name, "acme");
724 assert_eq!(pragma.version, "1.0.0");
725 assert_eq!(pragma.doc, "My lib");
726 }
727
728 #[test]
729 fn parse_dotted_type_refs() {
730 let file = parse_xeto("Ahu : ph.equips.Equip").unwrap();
731 let spec = &file.specs[0];
732 assert_eq!(spec.base.as_deref(), Some("ph.equips.Equip"));
733 }
734
735 #[test]
736 fn parse_qualified_type_refs() {
737 let file = parse_xeto("Ahu : ph::Equip").unwrap();
738 let spec = &file.specs[0];
739 assert_eq!(spec.base.as_deref(), Some("ph::Equip"));
740 }
741
742 #[test]
743 fn parse_defaults() {
744 let file = parse_xeto("Foo : Str \"hello\"").unwrap();
745 let spec = &file.specs[0];
746 assert_eq!(spec.default, Some(Kind::Str("hello".to_string())));
747 }
748
749 #[test]
750 fn parse_number_defaults() {
751 let file = parse_xeto("Foo : Number {\n val : Number 72.5\n}").unwrap();
752 let slot = &file.specs[0].slots[0];
753 assert_eq!(slot.default, Some(Kind::Number(Number::unitless(72.5))));
754 }
755
756 #[test]
757 fn parse_doc_comments() {
758 let src = "// This is an AHU\n// It handles air\nAhu : Equip";
759 let file = parse_xeto(src).unwrap();
760 let spec = &file.specs[0];
761 assert_eq!(spec.doc, "This is an AHU\nIt handles air");
762 }
763
764 #[test]
765 fn parse_section_separator_comments() {
766 let src = "////\n// Equips\n////\nAhu : Equip";
767 let file = parse_xeto(src).unwrap();
768 assert_eq!(file.specs.len(), 1);
769 assert_eq!(file.specs[0].name, "Ahu");
770 assert_eq!(file.specs[0].doc, "Equips");
771 }
772
773 #[test]
774 fn parse_multiple_specs() {
775 let src = "Ahu : Equip\nVav : Equip";
776 let file = parse_xeto(src).unwrap();
777 assert_eq!(file.specs.len(), 2);
778 assert_eq!(file.specs[0].name, "Ahu");
779 assert_eq!(file.specs[1].name, "Vav");
780 }
781
782 #[test]
783 fn parse_global_slots() {
784 let file = parse_xeto("Foo : Bar {\n *name : Str\n}").unwrap();
785 let slot = &file.specs[0].slots[0];
786 assert!(slot.is_global);
787 assert_eq!(slot.name, "name");
788 }
789
790 #[test]
791 fn parse_maybe_marker_slot() {
792 let file = parse_xeto("Foo : Bar {\n optional?\n}").unwrap();
793 let slot = &file.specs[0].slots[0];
794 assert!(slot.is_marker);
795 assert!(slot.is_maybe);
796 }
797
798 #[test]
799 fn parse_meta_with_typed_value() {
800 let src = r#"Foo : Bar <doc: "A foo", maxVal: 100>"#;
801 let file = parse_xeto(src).unwrap();
802 let spec = &file.specs[0];
803 assert_eq!(spec.meta.get("doc"), Some(&Kind::Str("A foo".to_string())));
804 assert_eq!(
805 spec.meta.get("maxVal"),
806 Some(&Kind::Number(Number::unitless(100.0)))
807 );
808 }
809
810 #[test]
811 fn parse_nested_body() {
812 let src = "Ahu : Equip {\n points : Query {\n temp : Point\n }\n}";
813 let file = parse_xeto(src).unwrap();
814 let slot = &file.specs[0].slots[0];
815 assert_eq!(slot.name, "points");
816 assert_eq!(slot.children.len(), 1);
817 assert_eq!(slot.children[0].name, "temp");
818 }
819
820 #[test]
821 fn parse_spec_no_base() {
822 let file = parse_xeto("Foo {\n bar\n}").unwrap();
823 let spec = &file.specs[0];
824 assert_eq!(spec.name, "Foo");
825 assert!(spec.base.is_none());
826 assert_eq!(spec.slots.len(), 1);
827 }
828
829 #[test]
830 fn parse_slot_doc_comments() {
831 let src = "Foo : Bar {\n // The name\n name : Str\n}";
832 let file = parse_xeto(src).unwrap();
833 let slot = &file.specs[0].slots[0];
834 assert_eq!(slot.doc, "The name");
835 }
836
837 #[test]
838 fn spec_colon_meta_no_base() {
839 let file = parse_xeto("Obj : <sealed> {}").unwrap();
840 let spec = &file.specs[0];
841 assert_eq!(spec.name, "Obj");
842 assert!(spec.base.is_none());
843 assert!(spec.meta.contains_key("sealed"));
844 }
845
846 #[test]
847 fn meta_dict_value() {
848 let src = r#"pragma : Lib <depends: { { lib: "ph" } }>"#;
849 let file = parse_xeto(src).unwrap();
850 let pragma = file.pragma.as_ref().unwrap();
851 assert_eq!(pragma.depends, vec!["ph"]);
852 }
853
854 #[test]
855 fn meta_parameterized_type() {
856 let src = r#"Foo : Bar <type: Ref<of:Spec>>"#;
857 let file = parse_xeto(src).unwrap();
858 let spec = &file.specs[0];
859 let type_val = spec.meta.get("type").unwrap();
860 if let Kind::Str(s) = type_val {
861 assert!(s.starts_with("Ref<"));
862 assert!(s.contains("of:"));
863 assert!(s.contains("Spec"));
864 assert!(s.ends_with(">"));
865 } else {
866 panic!("expected Str, got {:?}", type_val);
867 }
868 }
869
870 #[test]
871 fn slot_maybe_after_params() {
872 let file = parse_xeto("Foo : Bar {\n dis : Str?\n}").unwrap();
874 let slot = &file.specs[0].slots[0];
875 assert!(slot.is_maybe);
876
877 let file2 = parse_xeto("Foo : Bar {\n link : Ref <of: Equip>?\n}").unwrap();
879 let slot2 = &file2.specs[0].slots[0];
880 assert!(slot2.is_maybe);
881 }
882}