1use crate::ast::{AtomicKind, ParserAst, TemplatePart};
12use praxis_source::Span;
13use praxis_typeck::{
14 CollectionCtor, EnumVariantDef, FieldSet, TupleElems, Type, TypeCtorError, TypeDb, VariantSet,
15};
16
17pub fn synthesize(ast: &ParserAst, db: &mut TypeDb) -> Result<Type, TypeCtorError> {
27 let mut discard = Vec::new();
28 synth(ast, db, &mut discard)
29}
30
31pub fn synthesize_indexed(
47 ast: &ParserAst,
48 db: &mut TypeDb,
49) -> Result<(Type, Vec<(Span, Type)>), TypeCtorError> {
50 let mut out = Vec::new();
51 let ty = synth(ast, db, &mut out)?;
52 Ok((ty, out))
53}
54
55fn synth(
59 ast: &ParserAst,
60 db: &mut TypeDb,
61 out: &mut Vec<(Span, Type)>,
62) -> Result<Type, TypeCtorError> {
63 let ty = synth_inner(ast, db, out)?;
64 out.push((ast.span(), ty));
69 Ok(ty)
70}
71
72fn synth_inner(
73 ast: &ParserAst,
74 db: &mut TypeDb,
75 out: &mut Vec<(Span, Type)>,
76) -> Result<Type, TypeCtorError> {
77 Ok(match ast {
78 ParserAst::Atomic { kind, .. } => atomic_type(*kind, db),
79 ParserAst::Template { parts, .. } => template_type(parts, db, out)?,
80 ParserAst::Lines { child, .. }
81 | ParserAst::Sections { child, .. }
82 | ParserAst::Csv { child, .. }
83 | ParserAst::Ws { child, .. }
84 | ParserAst::Sep { child, .. } => {
85 let elem = synth(child, db, out)?;
90 db.vec(elem)
91 }
92 ParserAst::Grid { child, .. }
93 | ParserAst::Matrix { child, .. }
94 | ParserAst::GridRagged { child, .. } => {
95 let elem = synth(child, db, out)?;
99 db.unary_collection(CollectionCtor::Grid, elem)
100 }
101 ParserAst::SectionsNamed {
102 fields,
103 repeated_tail,
104 ..
105 } => {
106 let mut rec_fields: Vec<(String, Type)> = Vec::with_capacity(fields.len());
112 for item in fields {
113 let elem = synth(item.parser(), db, out)?;
114 let ty = match item {
115 crate::ast::SectionItem::One { .. } => elem,
116 crate::ast::SectionItem::Counted { .. } => db.vec(elem),
117 };
118 rec_fields.push((item.name().to_string(), ty));
119 }
120 if let Some((name, tail)) = repeated_tail {
121 let elem = synth(tail, db, out)?;
122 rec_fields.push((name.clone(), db.vec(elem)));
123 }
124 db.record(None, FieldSet::from_pairs(rec_fields)?)
125 }
126 ParserAst::Block { items, .. } => {
127 let mut rec_fields: Vec<(String, Type)> = Vec::new();
131 for item in items {
132 match item {
133 crate::ast::BlockItem::Positional(p) => {
134 if let ParserAst::Template { parts, .. } = p {
135 for part in parts {
136 if let TemplatePart::Capture {
137 name: Some(n),
138 parser,
139 ..
140 } = part
141 {
142 rec_fields
143 .push((n.as_str().to_string(), synth(parser, db, out)?));
144 }
145 }
146 }
147 }
148 crate::ast::BlockItem::Named { name, parser } => {
149 rec_fields.push((name.clone(), synth(parser, db, out)?));
150 }
151 }
152 }
153 db.record(None, FieldSet::from_pairs(rec_fields)?)
154 }
155 ParserAst::Choice { cases, .. } => {
156 let mut variants: Vec<EnumVariantDef> = Vec::with_capacity(cases.len());
161 for (name, p) in cases {
162 let payload_ty = synth(p, db, out)?;
163 variants.push(EnumVariantDef::new(name.clone(), vec![payload_ty]));
164 }
165 db.enum_(None, VariantSet::new(variants)?)
166 }
167 ParserAst::Optional { child, .. } => {
168 let elem = synth(child, db, out)?;
171 db.option_of(elem)
172 }
173 ParserAst::Scan { child, .. } => {
174 let elem = synth(child, db, out)?;
176 db.vec(elem)
177 }
178 ParserAst::OneOf { .. } => {
179 db.char()
181 }
182 ParserAst::Characters { child, .. } => {
183 let elem = synth(child, db, out)?;
189 db.vec(elem)
190 }
191 })
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum AtomicClass {
211 Int,
213 Float,
215 Byte,
217 Char,
219 Text,
221}
222
223impl AtomicClass {
224 pub fn of(kind: AtomicKind) -> AtomicClass {
234 match kind {
235 AtomicKind::Int | AtomicKind::UInt | AtomicKind::Digit => AtomicClass::Int,
236 AtomicKind::Float => AtomicClass::Float,
237 AtomicKind::Byte => AtomicClass::Byte,
238 AtomicKind::Char => AtomicClass::Char,
239 AtomicKind::Word | AtomicKind::Identifier | AtomicKind::Text | AtomicKind::Rest => {
240 AtomicClass::Text
241 }
242 }
243 }
244}
245
246fn atomic_type(kind: AtomicKind, db: &mut TypeDb) -> Type {
249 match AtomicClass::of(kind) {
250 AtomicClass::Int => db.int(),
251 AtomicClass::Float => db.float(),
252 AtomicClass::Byte => db.scalar(praxis_typeck::ScalarType::Byte),
253 AtomicClass::Char => db.char(),
254 AtomicClass::Text => db.text(),
255 }
256}
257
258fn template_type(
268 parts: &[TemplatePart],
269 db: &mut TypeDb,
270 out: &mut Vec<(Span, Type)>,
271) -> Result<Type, TypeCtorError> {
272 let captures: Vec<&TemplatePart> = parts
273 .iter()
274 .filter(|p| matches!(p, TemplatePart::Capture { .. }))
275 .collect();
276
277 if captures.is_empty() {
278 return Ok(db.unit());
280 }
281
282 let any_named = captures
283 .iter()
284 .any(|p| matches!(p, TemplatePart::Capture { name: Some(_), .. }));
285
286 if any_named {
287 return record_type(&captures, db, out);
289 }
290
291 let mut elem_types: Vec<Type> = Vec::with_capacity(captures.len());
293 for p in &captures {
294 let TemplatePart::Capture { parser, .. } = p else {
295 unreachable!("filtered to captures")
296 };
297 elem_types.push(synth(parser, db, out)?);
298 }
299 if elem_types.len() == 1 {
300 Ok(elem_types[0])
301 } else {
302 Ok(db.tuple(TupleElems::new(elem_types)?))
303 }
304}
305
306fn record_type(
313 captures: &[&TemplatePart],
314 db: &mut TypeDb,
315 out: &mut Vec<(Span, Type)>,
316) -> Result<Type, TypeCtorError> {
317 let mut fields = Vec::with_capacity(captures.len());
320 for part in captures {
321 match part {
322 TemplatePart::Capture { name, parser, .. } => {
323 let name_str = name
324 .as_ref()
325 .map(|n| n.as_str().to_string())
326 .unwrap_or_default();
327 fields.push((name_str, synth(parser, db, out)?));
328 }
329 _ => unreachable!("filtered to captures"),
330 }
331 }
332 Ok(db.record(None, FieldSet::from_pairs(fields)?))
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::ast::{AtomicKind, CaptureName, TemplatePart};
339 use praxis_source::Span;
340
341 fn atom(kind: AtomicKind) -> ParserAst {
342 ParserAst::Atomic {
343 kind,
344 span: Span::at(0),
345 }
346 }
347
348 #[test]
358 fn every_atomic_the_design_requires_has_a_type() {
359 use praxis_typeck::{ScalarType, TypeData};
360 let mut db = TypeDb::new();
361 for kind in AtomicKind::ALL {
362 let t = synthesize(&atom(*kind), &mut db).expect("an atomic synthesizes");
363 let expected = match kind {
364 AtomicKind::Int | AtomicKind::UInt | AtomicKind::Digit => ScalarType::Int,
365 AtomicKind::Float => ScalarType::Float,
366 AtomicKind::Byte => ScalarType::Byte,
367 AtomicKind::Char => ScalarType::Char,
368 AtomicKind::Word | AtomicKind::Identifier | AtomicKind::Text | AtomicKind::Rest => {
369 ScalarType::Text
370 }
371 };
372 match db.data(t) {
373 TypeData::Scalar(s) => assert_eq!(*s, expected, "for `{}`", kind.keyword()),
374 other => panic!("`{}` must be a scalar, got {other:?}", kind.keyword()),
375 }
376 assert!(
377 !matches!(db.data(t), TypeData::Scalar(ScalarType::UInt)),
378 "`{}` must not be typed UInt: it has no runtime object",
379 kind.keyword()
380 );
381 }
382 }
383
384 #[test]
385 fn atomic_int_synthesizes_int() {
386 let mut db = TypeDb::new();
387 let t = synthesize(&atom(AtomicKind::Int), &mut db).expect("int synthesizes");
388 assert!(matches!(
390 db.data(t),
391 praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
392 ));
393 }
394
395 #[test]
396 fn lines_of_int_is_vec_int() {
397 let mut db = TypeDb::new();
398 let ast = ParserAst::Lines {
399 child: Box::new(atom(AtomicKind::Int)),
400 span: Span::at(0),
401 };
402 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
403 match db.data(t) {
404 praxis_typeck::TypeData::Collection { ctor, args } => {
405 assert_eq!(*ctor, CollectionCtor::Vec);
406 assert_eq!(args.len(), 1);
407 assert!(
408 matches!(
409 db.data(args[0]),
410 praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
411 ),
412 "the Vec element must be Int, got {}",
413 db.render(args[0])
414 );
415 }
416 other => panic!("expected Vec, got {other:?}"),
417 }
418 }
419
420 #[test]
421 fn grid_of_char_is_grid_char() {
422 let mut db = TypeDb::new();
423 let ast = ParserAst::Grid {
424 child: Box::new(atom(AtomicKind::Char)),
425 span: Span::at(0),
426 };
427 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
428 match db.data(t) {
429 praxis_typeck::TypeData::Collection { ctor, args } => {
430 assert_eq!(*ctor, CollectionCtor::Grid);
431 assert_eq!(args.len(), 1);
432 assert!(
433 matches!(
434 db.data(args[0]),
435 praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Char)
436 ),
437 "the Grid element must be Char, got {}",
438 db.render(args[0])
439 );
440 }
441 other => panic!("expected Grid, got {other:?}"),
442 }
443 }
444
445 #[test]
446 fn nested_sections_lines_csv_int() {
447 let mut db = TypeDb::new();
449 let ast = ParserAst::Sections {
450 child: Box::new(ParserAst::Lines {
451 child: Box::new(ParserAst::Csv {
452 child: Box::new(atom(AtomicKind::Int)),
453 span: Span::at(0),
454 }),
455 span: Span::at(0),
456 }),
457 span: Span::at(0),
458 };
459 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
460 let mut current = t;
462 for level in 1..=3 {
463 let praxis_typeck::TypeData::Collection { ctor, args } = db.data(current) else {
464 panic!("level {level} should be Vec, got {}", db.render(current));
465 };
466 assert_eq!(*ctor, CollectionCtor::Vec, "wrong ctor at level {level}");
467 assert_eq!(args.len(), 1, "wrong arity at level {level}");
468 current = args[0];
469 }
470 assert!(
471 matches!(
472 db.data(current),
473 praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
474 ),
475 "nested leaf must be Int, got {}",
476 db.render(current)
477 );
478 }
479
480 #[test]
481 fn template_single_anonymous_capture_is_scalar() {
482 let mut db = TypeDb::new();
483 let ast = ParserAst::Template {
484 parts: vec![TemplatePart::Capture {
485 name: None,
486 parser: Box::new(atom(AtomicKind::Int)),
487 span: Span::at(0),
488 name_span: None,
489 }],
490 span: Span::at(0),
491 };
492 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
493 assert!(matches!(
495 db.data(t),
496 praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
497 ));
498 }
499
500 #[test]
501 fn template_two_anonymous_captures_is_tuple() {
502 let mut db = TypeDb::new();
503 let ast = ParserAst::Template {
504 parts: vec![
505 TemplatePart::Capture {
506 name: None,
507 parser: Box::new(atom(AtomicKind::Int)),
508 span: Span::at(0),
509 name_span: None,
510 },
511 TemplatePart::Capture {
512 name: None,
513 parser: Box::new(atom(AtomicKind::Int)),
514 span: Span::at(0),
515 name_span: None,
516 },
517 ],
518 span: Span::at(0),
519 };
520 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
521 assert!(matches!(db.data(t), praxis_typeck::TypeData::Tuple(_)));
522 }
523
524 #[test]
525 fn template_named_captures_synthesize_anonymous_record() {
526 let mut db = TypeDb::new();
528 let ast = ParserAst::Template {
529 parts: vec![
530 TemplatePart::Capture {
531 name: Some(CaptureName::parse("x").expect("an identifier")),
532 parser: Box::new(atom(AtomicKind::Int)),
533 span: Span::at(0),
534 name_span: None,
535 },
536 TemplatePart::Capture {
537 name: Some(CaptureName::parse("y").expect("an identifier")),
538 parser: Box::new(atom(AtomicKind::Int)),
539 span: Span::at(0),
540 name_span: None,
541 },
542 ],
543 span: Span::at(0),
544 };
545 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
546 let praxis_typeck::TypeData::Record { def, .. } = db.data(t) else {
547 panic!("expected Record, got {:?}", db.data(t));
548 };
549 let rdef = db.record_def(*def);
550 assert!(rdef.name.is_none(), "anonymous record has no name");
551 assert_eq!(rdef.arity(), 2);
552 let (idx, _) = rdef.field("x").expect("field x");
553 assert_eq!(idx, 0);
554 assert_eq!(db.render(t), "{ x: Int, y: Int }");
556 }
557
558 #[test]
559 fn lines_of_named_captures_is_vec_of_record() {
560 let mut db = TypeDb::new();
562 let ast = ParserAst::Lines {
563 child: Box::new(ParserAst::Template {
564 parts: vec![
565 TemplatePart::Capture {
566 name: Some(CaptureName::parse("x").expect("an identifier")),
567 parser: Box::new(atom(AtomicKind::Int)),
568 span: Span::at(0),
569 name_span: None,
570 },
571 TemplatePart::Capture {
572 name: Some(CaptureName::parse("y").expect("an identifier")),
573 parser: Box::new(atom(AtomicKind::Int)),
574 span: Span::at(0),
575 name_span: None,
576 },
577 ],
578 span: Span::at(0),
579 }),
580 span: Span::at(0),
581 };
582 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
583 match db.data(t) {
584 praxis_typeck::TypeData::Collection { ctor, args } => {
585 assert_eq!(*ctor, CollectionCtor::Vec);
586 assert_eq!(args.len(), 1);
587 assert!(matches!(
588 db.data(args[0]),
589 praxis_typeck::TypeData::Record { .. }
590 ));
591 }
592 other => panic!("expected Vec[Record], got {other:?}"),
593 }
594 assert_eq!(db.render(t), "Vec[{ x: Int, y: Int }]");
595 }
596
597 #[test]
602 fn a_counted_group_is_a_vec_field_in_the_position_it_was_written() {
603 use crate::ast::{RepeatCount, SectionItem};
604
605 let mut db = TypeDb::new();
606 let ast = ParserAst::SectionsNamed {
607 fields: vec![
608 SectionItem::Counted {
609 name: "shapes".to_string(),
610 count: RepeatCount::new(6).expect("six sections"),
611 parser: ParserAst::Lines {
612 child: Box::new(atom(AtomicKind::Int)),
613 span: Span::at(0),
614 },
615 },
616 SectionItem::One {
617 name: "regions".to_string(),
618 parser: ParserAst::Lines {
619 child: Box::new(atom(AtomicKind::Int)),
620 span: Span::at(0),
621 },
622 },
623 ],
624 repeated_tail: None,
625 span: Span::at(0),
626 };
627 let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
628 assert_eq!(db.render(t), "{ shapes: Vec[Vec[Int]], regions: Vec[Int] }");
629 }
630}