Add type-checking to classes/fields/constructors.

This commit is contained in:
Jesse Brault 2026-03-12 15:37:37 -05:00
parent 3c0bf948ac
commit 8082f4c2e6
8 changed files with 121 additions and 24 deletions

View File

@ -5,6 +5,7 @@ use crate::diagnostic::Diagnostic;
use crate::source_range::SourceRange;
use crate::symbol::class_symbol::ClassSymbol;
use crate::symbol_table::{SymbolInsertError, SymbolTable};
use std::collections::HashSet;
use std::rc::Rc;
pub struct Class {
@ -138,10 +139,6 @@ impl Class {
}
pub fn type_check(&mut self, symbol_table: &SymbolTable) -> Result<(), Vec<Diagnostic>> {
// We need to determine if fields are initialized or not (the latter is an error).
// First phase: check all fields, then check constructor, leaving pending those things that
// are fields <- initialized by constructor. Then circle back to fields and check all are
// initialized
let mut diagnostics: Vec<Diagnostic> = vec![];
self.fields
@ -151,7 +148,58 @@ impl Class {
.flatten()
.for_each(|diagnostic| diagnostics.push(diagnostic));
// todo check constructor and functions and re-check fields
if let Some(constructor) = &mut self.constructor {
match constructor.type_check(symbol_table) {
Ok(_) => {}
Err(mut constructor_diagnostics) => {
diagnostics.append(&mut constructor_diagnostics);
}
}
}
self.functions
.iter_mut()
.map(|function| function.type_check(symbol_table))
.filter_map(Result::err)
.flatten()
.for_each(|diagnostic| diagnostics.push(diagnostic));
// We need to determine if fields are initialized or not (the latter is an error).
// First phase: check all fields, then check constructor, leaving pending those things that
// are fields <- initialized by constructor. Then circle back to fields and check all are
// initialized
let mut initialized_field_names: HashSet<&str> = HashSet::new();
for field in &self.fields {
if field.initializer().is_some() {
initialized_field_names.insert(field.declared_name());
}
}
// check constructor
if let Some(constructor) = &self.constructor {
for statement in constructor.statements() {
match statement {
_ => {
// no-op for now, because we don't yet have assign statements
}
}
}
}
// check that all fields are present in the hash set
for field in &self.fields {
if !initialized_field_names.contains(field.declared_name()) {
diagnostics.push(
Diagnostic::new(
&format!("Field {} is not initialized.", field.declared_name()),
field.declared_name_source_range().start(),
field.declared_name_source_range().end(),
)
.with_reporter(file!(), line!()),
)
}
}
if diagnostics.is_empty() {
Ok(())
@ -167,11 +215,11 @@ mod tests {
use crate::parser::parse_compilation_unit;
#[test]
fn name_analysis_no_diagnostics() {
fn complete_example_no_diagnostics() {
let parse_result = parse_compilation_unit(
"
class Foo
mut bar: Int
mut bar: Int = 42
ctor(_bar: Int)
end
@ -210,5 +258,12 @@ mod tests {
panic!("{:?}", diagnostics);
}
}
match compilation_unit.type_check(&symbol_table) {
Ok(_) => {}
Err(diagnostics) => {
panic!("{:?}", diagnostics);
}
}
}
}

View File

@ -30,6 +30,10 @@ impl Constructor {
}
}
pub fn statements(&self) -> &[Statement] {
&self.statements
}
pub fn gather_declared_names(
&mut self,
symbol_table: &mut SymbolTable,
@ -124,4 +128,32 @@ impl Constructor {
Err(statements_diagnostics)
}
}
pub fn type_check(&mut self, symbol_table: &SymbolTable) -> Result<(), Vec<Diagnostic>> {
let parameters_diagnostics: Vec<Diagnostic> = self
.parameters
.iter_mut()
.map(|param| param.type_check(symbol_table))
.filter_map(Result::err)
.flatten()
.collect();
if !parameters_diagnostics.is_empty() {
return Err(parameters_diagnostics);
}
let statements_diagnostics: Vec<Diagnostic> = self
.statements
.iter_mut()
.map(|statement| statement.type_check(symbol_table, None))
.filter_map(Result::err)
.flatten()
.collect();
if statements_diagnostics.is_empty() {
Ok(())
} else {
Err(statements_diagnostics)
}
}
}

View File

@ -3,10 +3,8 @@ use crate::ast::ir_builder::IrBuilder;
use crate::diagnostic::Diagnostic;
use crate::ir::ir_return::IrReturn;
use crate::ir::ir_statement::IrStatement;
use crate::symbol::function_symbol::FunctionSymbol;
use crate::symbol_table::SymbolTable;
use std::cell::RefCell;
use std::rc::Rc;
use crate::type_info::TypeInfo;
pub struct ExpressionStatement {
expression: Box<Expression>,
@ -37,15 +35,13 @@ impl ExpressionStatement {
pub fn type_check(
&mut self,
symbol_table: &SymbolTable,
is_last: bool,
function_symbol: &Rc<RefCell<FunctionSymbol>>,
must_return_type_info: Option<&TypeInfo>,
) -> Result<(), Vec<Diagnostic>> {
self.expression.type_check(symbol_table)?;
if is_last {
if must_return_type_info.is_some() {
let expression_type = self.expression.type_info();
let borrowed_symbol = function_symbol.borrow();
let return_type = borrowed_symbol.return_type_info();
let return_type = must_return_type_info.unwrap();
if !return_type.is_assignable_from(expression_type) {
return Err(vec![Diagnostic::new(
&format!(

View File

@ -37,6 +37,18 @@ impl Field {
}
}
pub fn declared_name(&self) -> &str {
&self.declared_name
}
pub fn declared_name_source_range(&self) -> &SourceRange {
&self.declared_name_source_range
}
pub fn initializer(&self) -> Option<&Expression> {
self.initializer.as_ref().map(Box::as_ref)
}
pub fn gather_declared_names(
&mut self,
symbol_table: &mut SymbolTable,

View File

@ -208,6 +208,7 @@ impl Function {
);
let function_symbol = self.function_symbol.as_ref().unwrap();
let return_type_info = function_symbol.borrow().return_type_info().clone();
let statements_len = self.statements.len();
// statements
@ -218,7 +219,11 @@ impl Function {
.enumerate()
.map(|(i, statement)| {
let is_last = i == statements_len - 1;
statement.type_check(symbol_table, is_last, function_symbol)
if is_last {
statement.type_check(symbol_table, Some(&return_type_info))
} else {
statement.type_check(symbol_table, None)
}
})
.filter_map(Result::err)
.flatten()

View File

@ -36,7 +36,7 @@ impl Parameter {
let insert_result = symbol_table.insert_parameter_symbol(ParameterSymbol::new(
&self.declared_name,
self.declared_name_source_range.clone(),
TypeInfo::from_declared_name(self.type_use.declared_name()),
TypeInfo::from_declared_name(self.type_use.declared_name()), // todo: this will blow up if type is a Class
));
match insert_result {
Ok(parameter_symbol) => {

View File

@ -2,10 +2,8 @@ use crate::ast::expression_statement::ExpressionStatement;
use crate::ast::ir_builder::IrBuilder;
use crate::ast::let_statement::LetStatement;
use crate::diagnostic::Diagnostic;
use crate::symbol::function_symbol::FunctionSymbol;
use crate::symbol_table::SymbolTable;
use std::cell::RefCell;
use std::rc::Rc;
use crate::type_info::TypeInfo;
pub enum Statement {
Let(LetStatement),
@ -37,13 +35,12 @@ impl Statement {
pub fn type_check(
&mut self,
symbol_table: &SymbolTable,
is_last: bool,
function_symbol: &Rc<RefCell<FunctionSymbol>>,
must_return_type_info: Option<&TypeInfo>,
) -> Result<(), Vec<Diagnostic>> {
match self {
Statement::Let(let_statement) => let_statement.type_check(symbol_table),
Statement::Expression(expression_statement) => {
expression_statement.type_check(symbol_table, is_last, function_symbol)
expression_statement.type_check(symbol_table, must_return_type_info)
}
}
}

View File

@ -56,7 +56,7 @@ impl TypeUse {
}
}
pub fn type_check(&mut self, symbol_table: &SymbolTable) -> Result<(), Vec<Diagnostic>> {
pub fn type_check(&mut self, _symbol_table: &SymbolTable) -> Result<(), Vec<Diagnostic>> {
Ok(()) // no-op, for now
}