use crate::ast::{AtomicKind, ParserAst, TemplatePart};
use praxis_source::Span;
use praxis_typeck::{
CollectionCtor, EnumVariantDef, FieldSet, TupleElems, Type, TypeCtorError, TypeDb, VariantSet,
};
pub fn synthesize(ast: &ParserAst, db: &mut TypeDb) -> Result<Type, TypeCtorError> {
let mut discard = Vec::new();
synth(ast, db, &mut discard)
}
pub fn synthesize_indexed(
ast: &ParserAst,
db: &mut TypeDb,
) -> Result<(Type, Vec<(Span, Type)>), TypeCtorError> {
let mut out = Vec::new();
let ty = synth(ast, db, &mut out)?;
Ok((ty, out))
}
fn synth(
ast: &ParserAst,
db: &mut TypeDb,
out: &mut Vec<(Span, Type)>,
) -> Result<Type, TypeCtorError> {
let ty = synth_inner(ast, db, out)?;
out.push((ast.span(), ty));
Ok(ty)
}
fn synth_inner(
ast: &ParserAst,
db: &mut TypeDb,
out: &mut Vec<(Span, Type)>,
) -> Result<Type, TypeCtorError> {
Ok(match ast {
ParserAst::Atomic { kind, .. } => atomic_type(*kind, db),
ParserAst::Template { parts, .. } => template_type(parts, db, out)?,
ParserAst::Lines { child, .. }
| ParserAst::Sections { child, .. }
| ParserAst::Csv { child, .. }
| ParserAst::Ws { child, .. }
| ParserAst::Sep { child, .. } => {
let elem = synth(child, db, out)?;
db.vec(elem)
}
ParserAst::Grid { child, .. }
| ParserAst::Matrix { child, .. }
| ParserAst::GridRagged { child, .. } => {
let elem = synth(child, db, out)?;
db.unary_collection(CollectionCtor::Grid, elem)
}
ParserAst::SectionsNamed {
fields,
repeated_tail,
..
} => {
let mut rec_fields: Vec<(String, Type)> = Vec::with_capacity(fields.len());
for item in fields {
let elem = synth(item.parser(), db, out)?;
let ty = match item {
crate::ast::SectionItem::One { .. } => elem,
crate::ast::SectionItem::Counted { .. } => db.vec(elem),
};
rec_fields.push((item.name().to_string(), ty));
}
if let Some((name, tail)) = repeated_tail {
let elem = synth(tail, db, out)?;
rec_fields.push((name.clone(), db.vec(elem)));
}
db.record(None, FieldSet::from_pairs(rec_fields)?)
}
ParserAst::Block { items, .. } => {
let mut rec_fields: Vec<(String, Type)> = Vec::new();
for item in items {
match item {
crate::ast::BlockItem::Positional(p) => {
if let ParserAst::Template { parts, .. } = p {
for part in parts {
if let TemplatePart::Capture {
name: Some(n),
parser,
..
} = part
{
rec_fields
.push((n.as_str().to_string(), synth(parser, db, out)?));
}
}
}
}
crate::ast::BlockItem::Named { name, parser } => {
rec_fields.push((name.clone(), synth(parser, db, out)?));
}
}
}
db.record(None, FieldSet::from_pairs(rec_fields)?)
}
ParserAst::Choice { cases, .. } => {
let mut variants: Vec<EnumVariantDef> = Vec::with_capacity(cases.len());
for (name, p) in cases {
let payload_ty = synth(p, db, out)?;
variants.push(EnumVariantDef::new(name.clone(), vec![payload_ty]));
}
db.enum_(None, VariantSet::new(variants)?)
}
ParserAst::Optional { child, .. } => {
let elem = synth(child, db, out)?;
db.option_of(elem)
}
ParserAst::Scan { child, .. } => {
let elem = synth(child, db, out)?;
db.vec(elem)
}
ParserAst::OneOf { .. } => {
db.char()
}
ParserAst::Characters { child, .. } => {
let elem = synth(child, db, out)?;
db.vec(elem)
}
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AtomicClass {
Int,
Float,
Byte,
Char,
Text,
}
impl AtomicClass {
pub fn of(kind: AtomicKind) -> AtomicClass {
match kind {
AtomicKind::Int | AtomicKind::UInt | AtomicKind::Digit => AtomicClass::Int,
AtomicKind::Float => AtomicClass::Float,
AtomicKind::Byte => AtomicClass::Byte,
AtomicKind::Char => AtomicClass::Char,
AtomicKind::Word | AtomicKind::Identifier | AtomicKind::Text | AtomicKind::Rest => {
AtomicClass::Text
}
}
}
}
fn atomic_type(kind: AtomicKind, db: &mut TypeDb) -> Type {
match AtomicClass::of(kind) {
AtomicClass::Int => db.int(),
AtomicClass::Float => db.float(),
AtomicClass::Byte => db.scalar(praxis_typeck::ScalarType::Byte),
AtomicClass::Char => db.char(),
AtomicClass::Text => db.text(),
}
}
fn template_type(
parts: &[TemplatePart],
db: &mut TypeDb,
out: &mut Vec<(Span, Type)>,
) -> Result<Type, TypeCtorError> {
let captures: Vec<&TemplatePart> = parts
.iter()
.filter(|p| matches!(p, TemplatePart::Capture { .. }))
.collect();
if captures.is_empty() {
return Ok(db.unit());
}
let any_named = captures
.iter()
.any(|p| matches!(p, TemplatePart::Capture { name: Some(_), .. }));
if any_named {
return record_type(&captures, db, out);
}
let mut elem_types: Vec<Type> = Vec::with_capacity(captures.len());
for p in &captures {
let TemplatePart::Capture { parser, .. } = p else {
unreachable!("filtered to captures")
};
elem_types.push(synth(parser, db, out)?);
}
if elem_types.len() == 1 {
Ok(elem_types[0])
} else {
Ok(db.tuple(TupleElems::new(elem_types)?))
}
}
fn record_type(
captures: &[&TemplatePart],
db: &mut TypeDb,
out: &mut Vec<(Span, Type)>,
) -> Result<Type, TypeCtorError> {
let mut fields = Vec::with_capacity(captures.len());
for part in captures {
match part {
TemplatePart::Capture { name, parser, .. } => {
let name_str = name
.as_ref()
.map(|n| n.as_str().to_string())
.unwrap_or_default();
fields.push((name_str, synth(parser, db, out)?));
}
_ => unreachable!("filtered to captures"),
}
}
Ok(db.record(None, FieldSet::from_pairs(fields)?))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{AtomicKind, CaptureName, TemplatePart};
use praxis_source::Span;
fn atom(kind: AtomicKind) -> ParserAst {
ParserAst::Atomic {
kind,
span: Span::at(0),
}
}
#[test]
fn every_atomic_the_design_requires_has_a_type() {
use praxis_typeck::{ScalarType, TypeData};
let mut db = TypeDb::new();
for kind in AtomicKind::ALL {
let t = synthesize(&atom(*kind), &mut db).expect("an atomic synthesizes");
let expected = match kind {
AtomicKind::Int | AtomicKind::UInt | AtomicKind::Digit => ScalarType::Int,
AtomicKind::Float => ScalarType::Float,
AtomicKind::Byte => ScalarType::Byte,
AtomicKind::Char => ScalarType::Char,
AtomicKind::Word | AtomicKind::Identifier | AtomicKind::Text | AtomicKind::Rest => {
ScalarType::Text
}
};
match db.data(t) {
TypeData::Scalar(s) => assert_eq!(*s, expected, "for `{}`", kind.keyword()),
other => panic!("`{}` must be a scalar, got {other:?}", kind.keyword()),
}
assert!(
!matches!(db.data(t), TypeData::Scalar(ScalarType::UInt)),
"`{}` must not be typed UInt: it has no runtime object",
kind.keyword()
);
}
}
#[test]
fn atomic_int_synthesizes_int() {
let mut db = TypeDb::new();
let t = synthesize(&atom(AtomicKind::Int), &mut db).expect("int synthesizes");
assert!(matches!(
db.data(t),
praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
));
}
#[test]
fn lines_of_int_is_vec_int() {
let mut db = TypeDb::new();
let ast = ParserAst::Lines {
child: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
match db.data(t) {
praxis_typeck::TypeData::Collection { ctor, args } => {
assert_eq!(*ctor, CollectionCtor::Vec);
assert_eq!(args.len(), 1);
assert!(
matches!(
db.data(args[0]),
praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
),
"the Vec element must be Int, got {}",
db.render(args[0])
);
}
other => panic!("expected Vec, got {other:?}"),
}
}
#[test]
fn grid_of_char_is_grid_char() {
let mut db = TypeDb::new();
let ast = ParserAst::Grid {
child: Box::new(atom(AtomicKind::Char)),
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
match db.data(t) {
praxis_typeck::TypeData::Collection { ctor, args } => {
assert_eq!(*ctor, CollectionCtor::Grid);
assert_eq!(args.len(), 1);
assert!(
matches!(
db.data(args[0]),
praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Char)
),
"the Grid element must be Char, got {}",
db.render(args[0])
);
}
other => panic!("expected Grid, got {other:?}"),
}
}
#[test]
fn nested_sections_lines_csv_int() {
let mut db = TypeDb::new();
let ast = ParserAst::Sections {
child: Box::new(ParserAst::Lines {
child: Box::new(ParserAst::Csv {
child: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
}),
span: Span::at(0),
}),
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
let mut current = t;
for level in 1..=3 {
let praxis_typeck::TypeData::Collection { ctor, args } = db.data(current) else {
panic!("level {level} should be Vec, got {}", db.render(current));
};
assert_eq!(*ctor, CollectionCtor::Vec, "wrong ctor at level {level}");
assert_eq!(args.len(), 1, "wrong arity at level {level}");
current = args[0];
}
assert!(
matches!(
db.data(current),
praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
),
"nested leaf must be Int, got {}",
db.render(current)
);
}
#[test]
fn template_single_anonymous_capture_is_scalar() {
let mut db = TypeDb::new();
let ast = ParserAst::Template {
parts: vec![TemplatePart::Capture {
name: None,
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
}],
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
assert!(matches!(
db.data(t),
praxis_typeck::TypeData::Scalar(praxis_typeck::ScalarType::Int)
));
}
#[test]
fn template_two_anonymous_captures_is_tuple() {
let mut db = TypeDb::new();
let ast = ParserAst::Template {
parts: vec![
TemplatePart::Capture {
name: None,
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
},
TemplatePart::Capture {
name: None,
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
},
],
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
assert!(matches!(db.data(t), praxis_typeck::TypeData::Tuple(_)));
}
#[test]
fn template_named_captures_synthesize_anonymous_record() {
let mut db = TypeDb::new();
let ast = ParserAst::Template {
parts: vec![
TemplatePart::Capture {
name: Some(CaptureName::parse("x").expect("an identifier")),
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
},
TemplatePart::Capture {
name: Some(CaptureName::parse("y").expect("an identifier")),
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
},
],
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
let praxis_typeck::TypeData::Record { def, .. } = db.data(t) else {
panic!("expected Record, got {:?}", db.data(t));
};
let rdef = db.record_def(*def);
assert!(rdef.name.is_none(), "anonymous record has no name");
assert_eq!(rdef.arity(), 2);
let (idx, _) = rdef.field("x").expect("field x");
assert_eq!(idx, 0);
assert_eq!(db.render(t), "{ x: Int, y: Int }");
}
#[test]
fn lines_of_named_captures_is_vec_of_record() {
let mut db = TypeDb::new();
let ast = ParserAst::Lines {
child: Box::new(ParserAst::Template {
parts: vec![
TemplatePart::Capture {
name: Some(CaptureName::parse("x").expect("an identifier")),
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
},
TemplatePart::Capture {
name: Some(CaptureName::parse("y").expect("an identifier")),
parser: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
name_span: None,
},
],
span: Span::at(0),
}),
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
match db.data(t) {
praxis_typeck::TypeData::Collection { ctor, args } => {
assert_eq!(*ctor, CollectionCtor::Vec);
assert_eq!(args.len(), 1);
assert!(matches!(
db.data(args[0]),
praxis_typeck::TypeData::Record { .. }
));
}
other => panic!("expected Vec[Record], got {other:?}"),
}
assert_eq!(db.render(t), "Vec[{ x: Int, y: Int }]");
}
#[test]
fn a_counted_group_is_a_vec_field_in_the_position_it_was_written() {
use crate::ast::{RepeatCount, SectionItem};
let mut db = TypeDb::new();
let ast = ParserAst::SectionsNamed {
fields: vec![
SectionItem::Counted {
name: "shapes".to_string(),
count: RepeatCount::new(6).expect("six sections"),
parser: ParserAst::Lines {
child: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
},
},
SectionItem::One {
name: "regions".to_string(),
parser: ParserAst::Lines {
child: Box::new(atom(AtomicKind::Int)),
span: Span::at(0),
},
},
],
repeated_tail: None,
span: Span::at(0),
};
let t = synthesize(&ast, &mut db).expect("a valid AST synthesizes");
assert_eq!(db.render(t), "{ shapes: Vec[Vec[Int]], regions: Vec[Int] }");
}
}