refactor: split project into multiple crates
This commit is contained in:
parent
486af67fc2
commit
857f747524
27 changed files with 308 additions and 222 deletions
11
lila-checking/Cargo.toml
Normal file
11
lila-checking/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "lila-checking"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lila-ast = { path = "../lila-ast" }
|
||||
|
||||
[dev-dependencies]
|
||||
lila-parsing = { path = "../lila-parsing" }
|
||||
pretty_assertions = "1.4.0"
|
||||
395
lila-checking/src/lib.rs
Normal file
395
lila-checking/src/lib.rs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use lila_ast::typing::error::{BinOpAndSpan, TypeAndSpan, TypeError, TypeErrorKind};
|
||||
use lila_ast::typing::{Signature, Type};
|
||||
use lila_ast::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct CheckedModule(pub Module);
|
||||
|
||||
pub trait TypeCheckModule {
|
||||
fn type_check(&mut self) -> Result<(), Vec<TypeError>>;
|
||||
}
|
||||
|
||||
impl TypeCheckModule for Module {
|
||||
fn type_check(&mut self) -> Result<(), Vec<TypeError>> {
|
||||
let mut ctx = TypingContext::new(self.path.clone());
|
||||
ctx.file.clone_from(&self.file);
|
||||
|
||||
// Register all function signatures
|
||||
for func in &self.functions {
|
||||
if let Some(_previous) = ctx.functions.insert(func.name.clone(), func.signature()) {
|
||||
todo!("handle redefinition of function or identical function names across different files");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add signatures of imported functions (even if they have not been checked)
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// Type-check the function bodies and complete all type placeholders
|
||||
for func in &mut self.functions {
|
||||
if let Err(e) = func.typ(&mut ctx) {
|
||||
errors.push(e);
|
||||
};
|
||||
ctx.variables.clear();
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypingContext {
|
||||
pub file: Option<std::path::PathBuf>,
|
||||
pub module: ModulePath,
|
||||
pub function: Option<Identifier>,
|
||||
pub functions: HashMap<Identifier, Signature>,
|
||||
pub variables: HashMap<Identifier, Type>,
|
||||
}
|
||||
|
||||
impl TypingContext {
|
||||
pub fn new(path: ModulePath) -> Self {
|
||||
let builtin_functions = HashMap::from([(
|
||||
String::from("println"),
|
||||
Signature(vec![Type::Str], Type::Unit),
|
||||
)]);
|
||||
|
||||
Self {
|
||||
file: None,
|
||||
module: path,
|
||||
function: None,
|
||||
functions: builtin_functions,
|
||||
variables: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_error(&self, kind: TypeErrorKind) -> TypeError {
|
||||
TypeError {
|
||||
kind,
|
||||
file: self.file.clone(),
|
||||
module: self.module.clone(),
|
||||
function: self.function.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for nodes which have a deducible type.
|
||||
pub trait TypeCheck {
|
||||
/// Try to resolve the type of the node and complete its type placeholders.
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError>;
|
||||
}
|
||||
|
||||
impl TypeCheck for FunctionDefinition {
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError> {
|
||||
ctx.function = Some(self.name.clone());
|
||||
|
||||
for param in &self.parameters {
|
||||
// XXX: Parameter types should be checked
|
||||
// when they are not builtin
|
||||
ctx.variables.insert(param.name.clone(), param.typ.clone());
|
||||
}
|
||||
|
||||
let body_type = self.body.typ(ctx)?;
|
||||
|
||||
// Check coherence with the body's type.
|
||||
if *self.return_type.as_ref().unwrap_or(&Type::Unit) != body_type {
|
||||
return Err(
|
||||
ctx.make_error(TypeErrorKind::BlockTypeDoesNotMatchFunctionType {
|
||||
block_type: body_type.clone(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(self.return_type.clone().unwrap_or(Type::Unit))
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeCheck for Block {
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError> {
|
||||
// Check declarations and assignments.
|
||||
for statement in &mut self.statements {
|
||||
match statement {
|
||||
Statement::DeclareStatement {
|
||||
lhs: ident,
|
||||
rhs: expr,
|
||||
..
|
||||
} => {
|
||||
let typ = expr.typ(ctx)?;
|
||||
if let Some(_typ) = ctx.variables.insert(ident.clone(), typ.clone()) {
|
||||
// TODO: Shadowing? (illegal for now)
|
||||
return Err(ctx.make_error(TypeErrorKind::VariableRedeclaration));
|
||||
}
|
||||
}
|
||||
Statement::AssignStatement {
|
||||
lhs: ident,
|
||||
rhs: expr,
|
||||
..
|
||||
} => {
|
||||
let rhs_typ = expr.typ(ctx)?;
|
||||
let Some(lhs_typ) = ctx.variables.get(ident) else {
|
||||
return Err(ctx.make_error(TypeErrorKind::AssignUndeclared));
|
||||
};
|
||||
|
||||
// Ensure same type on both sides.
|
||||
if rhs_typ != *lhs_typ {
|
||||
return Err(ctx.make_error(TypeErrorKind::AssignmentMismatch {
|
||||
lht: lhs_typ.clone(),
|
||||
rht: rhs_typ.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Statement::ReturnStatement(return_stmt) => {
|
||||
return_stmt.typ(ctx)?;
|
||||
}
|
||||
Statement::CallStatement { call, span: _ } => {
|
||||
call.typ(ctx)?;
|
||||
}
|
||||
Statement::UseStatement { .. } => {
|
||||
// TODO: import the signatures (and types)
|
||||
todo!()
|
||||
}
|
||||
Statement::IfStatement {
|
||||
condition: cond,
|
||||
then_block: block,
|
||||
..
|
||||
} => {
|
||||
if cond.typ(ctx)? != Type::Bool {
|
||||
return Err(ctx.make_error(TypeErrorKind::ConditionIsNotBool));
|
||||
}
|
||||
block.typ(ctx)?;
|
||||
}
|
||||
Statement::WhileStatement {
|
||||
condition: cond,
|
||||
loop_block: block,
|
||||
span: _,
|
||||
} => {
|
||||
if cond.typ(ctx)? != Type::Bool {
|
||||
return Err(ctx.make_error(TypeErrorKind::ConditionIsNotBool));
|
||||
}
|
||||
block.typ(ctx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is an expression at the end of the block.
|
||||
if let Some(expr) = &mut self.value {
|
||||
self.typ = expr.typ(ctx)?.clone();
|
||||
Ok(self.typ.clone())
|
||||
} else {
|
||||
self.typ = Type::Unit;
|
||||
Ok(Type::Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeCheck for Call {
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError> {
|
||||
match &mut self.callee.expr {
|
||||
Expr::Identifier { name, typ, .. } => {
|
||||
let signature = match ctx.functions.get(name) {
|
||||
Some(sgn) => sgn.clone(),
|
||||
None => {
|
||||
return Err(
|
||||
ctx.make_error(TypeErrorKind::UnknownFunctionCalled(name.clone()))
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
*typ = signature.clone().into();
|
||||
|
||||
let Signature(params_types, func_type) = signature;
|
||||
|
||||
self.typ = func_type.clone();
|
||||
|
||||
// Collect arg types.
|
||||
let mut args_types: Vec<Type> = vec![];
|
||||
for arg in &mut self.args {
|
||||
let arg_typ = arg.typ(ctx)?;
|
||||
args_types.push(arg_typ.clone());
|
||||
}
|
||||
|
||||
if args_types == *params_types {
|
||||
Ok(self.typ.clone())
|
||||
} else {
|
||||
Err(ctx.make_error(TypeErrorKind::WrongFunctionArguments))
|
||||
}
|
||||
}
|
||||
_ => unimplemented!("cannot call on expression other than identifier"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeCheck for Expr {
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError> {
|
||||
match self {
|
||||
Expr::Identifier { name, typ, .. } => {
|
||||
if let Some(ty) = ctx.variables.get(name) {
|
||||
*typ = ty.clone();
|
||||
Ok(typ.clone())
|
||||
} else {
|
||||
Err(ctx.make_error(TypeErrorKind::UnknownIdentifier {
|
||||
identifier: name.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
Expr::BooleanLiteral(..) => Ok(Type::Bool),
|
||||
Expr::IntegerLiteral(..) => Ok(Type::Int),
|
||||
Expr::FloatLiteral(..) => Ok(Type::Float),
|
||||
Expr::UnaryExpression { op, inner, .. } => {
|
||||
let inner_type = &inner.typ(ctx)?;
|
||||
match (&op, inner_type) {
|
||||
(UnaryOperator::Not, Type::Bool) => Ok(Type::Bool),
|
||||
_ => Err(ctx.make_error(TypeErrorKind::InvalidUnaryOperator {
|
||||
operator: *op,
|
||||
inner: inner_type.clone(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
Expr::BinaryExpression(BinaryExpression {
|
||||
lhs,
|
||||
op,
|
||||
rhs,
|
||||
typ,
|
||||
op_span,
|
||||
}) => {
|
||||
let operator = BinOpAndSpan {
|
||||
op: op.clone(),
|
||||
span: *op_span,
|
||||
};
|
||||
let ty = match op {
|
||||
BinaryOperator::Add
|
||||
| BinaryOperator::Sub
|
||||
| BinaryOperator::Mul
|
||||
| BinaryOperator::Div
|
||||
| BinaryOperator::And
|
||||
| BinaryOperator::Or => {
|
||||
let left_type = &lhs.typ(ctx)?;
|
||||
let right_type = &rhs.typ(ctx)?;
|
||||
|
||||
match (left_type, right_type) {
|
||||
(Type::Int, Type::Int) => Ok(Type::Int),
|
||||
(Type::Float, Type::Float) => Ok(Type::Float),
|
||||
(Type::Bool, Type::Bool) => Ok(Type::Bool),
|
||||
(_, _) => Err(ctx.make_error(TypeErrorKind::InvalidBinaryOperator {
|
||||
operator,
|
||||
lhs: TypeAndSpan {
|
||||
ty: left_type.clone(),
|
||||
span: lhs.span,
|
||||
},
|
||||
rhs: TypeAndSpan {
|
||||
ty: right_type.clone(),
|
||||
span: rhs.span,
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
BinaryOperator::Equal | BinaryOperator::NotEqual => {
|
||||
let lhs_type = lhs.typ(ctx)?;
|
||||
let rhs_type = rhs.typ(ctx)?;
|
||||
if lhs_type != rhs_type {
|
||||
return Err(ctx.make_error(TypeErrorKind::InvalidBinaryOperator {
|
||||
operator,
|
||||
lhs: TypeAndSpan {
|
||||
ty: lhs_type.clone(),
|
||||
span: lhs.span,
|
||||
},
|
||||
rhs: TypeAndSpan {
|
||||
ty: rhs_type.clone(),
|
||||
span: rhs.span,
|
||||
},
|
||||
}));
|
||||
}
|
||||
Ok(Type::Bool)
|
||||
}
|
||||
BinaryOperator::Modulo => {
|
||||
let lhs_type = lhs.typ(ctx)?;
|
||||
let rhs_type = lhs.typ(ctx)?;
|
||||
match (&lhs_type, &rhs_type) {
|
||||
(Type::Int, Type::Int) => Ok(Type::Int),
|
||||
_ => Err(ctx.make_error(TypeErrorKind::InvalidBinaryOperator {
|
||||
operator,
|
||||
lhs: TypeAndSpan {
|
||||
ty: lhs_type.clone(),
|
||||
span: lhs.span,
|
||||
},
|
||||
rhs: TypeAndSpan {
|
||||
ty: rhs_type.clone(),
|
||||
span: rhs.span,
|
||||
},
|
||||
})),
|
||||
}
|
||||
}
|
||||
};
|
||||
*typ = ty?;
|
||||
Ok(typ.clone())
|
||||
}
|
||||
Expr::StringLiteral(_) => Ok(Type::Str),
|
||||
Expr::UnitLiteral => Ok(Type::Unit),
|
||||
Expr::Call(call) => call.typ(ctx),
|
||||
Expr::Block(block) => block.typ(ctx),
|
||||
Expr::IfExpr {
|
||||
cond,
|
||||
then_body,
|
||||
else_body,
|
||||
typ,
|
||||
} => {
|
||||
if cond.typ(ctx)? != Type::Bool {
|
||||
Err(ctx.make_error(TypeErrorKind::ConditionIsNotBool))
|
||||
} else {
|
||||
let then_body_type = then_body.typ(ctx)?;
|
||||
let else_type = else_body.typ(ctx)?;
|
||||
if then_body_type != else_type {
|
||||
Err(ctx.make_error(TypeErrorKind::IfElseMismatch))
|
||||
} else {
|
||||
// XXX: opt: return ref to avoid cloning
|
||||
*typ = then_body_type.clone();
|
||||
Ok(then_body_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeCheck for ReturnStatement {
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError> {
|
||||
let ty = if let Some(expr) = &mut self.expr {
|
||||
expr.typ(ctx)?
|
||||
} else {
|
||||
Type::Unit
|
||||
};
|
||||
|
||||
// Check if the returned type is coherent with the function's signature
|
||||
let func_type = &ctx.functions.get(ctx.function.as_ref().unwrap()).unwrap().1;
|
||||
if ty != *func_type {
|
||||
return Err(
|
||||
ctx.make_error(TypeErrorKind::ReturnTypeDoesNotMatchFunctionType {
|
||||
return_expr: self.expr.as_ref().map(|e| TypeAndSpan {
|
||||
ty: ty.clone(),
|
||||
span: e.span,
|
||||
}),
|
||||
return_stmt: TypeAndSpan {
|
||||
ty: ty.clone(),
|
||||
span: self.span,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
Ok(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeCheck for SExpr {
|
||||
#[inline]
|
||||
fn typ(&mut self, ctx: &mut TypingContext) -> Result<Type, TypeError> {
|
||||
self.expr.typ(ctx)
|
||||
}
|
||||
}
|
||||
36
lila-checking/src/tests.rs
Normal file
36
lila-checking/src/tests.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::TypeCheckModule;
|
||||
use lila_ast::typing::{error::*, Type};
|
||||
use lila_ast::ModulePath;
|
||||
use lila_parsing::{DefaultParser, Parser};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn addition_int_and_float() {
|
||||
let source = "fn add(a: int, b: float) int { a + b }";
|
||||
let mut ast = DefaultParser::default()
|
||||
.parse_as_module(source, ModulePath::default(), 0)
|
||||
.unwrap();
|
||||
let res = ast.type_check();
|
||||
assert!(res.is_err_and(|errors| errors.len() == 1
|
||||
&& matches!(errors[0].kind, TypeErrorKind::InvalidBinaryOperator { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_int_instead_of_float() {
|
||||
let source = "fn add(a: int, b: int) float { a + b }";
|
||||
let mut ast = DefaultParser::default()
|
||||
.parse_as_module(source, ModulePath::default(), 0)
|
||||
.unwrap();
|
||||
let res = ast.type_check();
|
||||
assert_eq!(
|
||||
res,
|
||||
Err(vec![TypeError {
|
||||
file: None,
|
||||
module: ModulePath::default(),
|
||||
function: Some("add".to_string()),
|
||||
kind: TypeErrorKind::BlockTypeDoesNotMatchFunctionType {
|
||||
block_type: Type::Int,
|
||||
}
|
||||
}])
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue