diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af9a032..e2d566d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,8 +83,9 @@ jobs: - name: Rustup toolchain install uses: dtolnay/rust-toolchain@1.85.0 - uses: homebrew/actions/setup-homebrew@master - - name: Retreive cached dependecies - uses: Swatinem/rust-cache@v2 + # makes melior be bugged with ods due to paths changing probably + #- name: Retreive cached dependecies + #uses: Swatinem/rust-cache@v2 - name: install llvm run: brew install llvm@19 - name: Run tests diff --git a/Cargo.lock b/Cargo.lock index 5623c26..e7192b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,7 @@ dependencies = [ "itertools 0.14.0", "lalrpop", "lalrpop-util", + "libloading", "llvm-sys", "logos", "logos-display", diff --git a/Cargo.toml b/Cargo.toml index 4416200..af24a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ toml = "0.8.19" test-case = "3.3.1" typed-generational-arena = "0.2.6" +libloading = "0.8" + [build-dependencies] lalrpop = "0.22.0" diff --git a/Makefile b/Makefile index a237936..07742a1 100644 --- a/Makefile +++ b/Makefile @@ -40,8 +40,8 @@ clean: .PHONY: test test: check-deps cargo test --all-targets --all-features && \ - echo "Testing example concrete project" && cd examples/project && cargo run -- build \ - && cd ../../std && cargo run -- build + echo "Testing example concrete project" && cd examples/project && cargo run -- build && \ + echo "Testing concrete std" && cd ../../std && cargo run -- test .PHONY: coverage coverage: check-deps diff --git a/src/check/mod.rs b/src/check/mod.rs index 03b9297..a16a73e 100644 --- a/src/check/mod.rs +++ b/src/check/mod.rs @@ -192,6 +192,25 @@ pub fn lowering_error_to_report(error: LoweringError) -> Report<'static, FileSpa .with_message(format!("expected type {}", expected)) .finish() } + LoweringError::InvalidUnaryOp { + found_span: span, + found, + path, + } => { + let path = path.display().to_string(); + let filespan = FileSpan::new(path.clone(), span.from..span.to); + let labels = vec![ + Label::new(filespan.clone()) + .with_message(format!("Invalid binary operation type '{}'", found)) + .with_color(colors.next()), + ]; + + Report::build(ReportKind::Error, filespan.clone()) + .with_code("InvalidUnaryOp") + .with_labels(labels) + .with_message(format!("invalid binary operation type {}", found)) + .finish() + } LoweringError::UseOfUndeclaredVariable { span, name, path } => { let path = path.display().to_string(); let filespan = FileSpan::new(path, span.from..span.to); diff --git a/src/codegen/compiler.rs b/src/codegen/compiler.rs index c5f68cb..6dca2c5 100644 --- a/src/codegen/compiler.rs +++ b/src/codegen/compiler.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use crate::ir::{ AdtKind, BinOp, ConcreteIntrinsic, ConstValue, FnIndex, Function, IR, LocalKind, Module, - ModuleIndex, Operand, Place, PlaceElem, Rvalue, Span, Type as IRType, TypeIndex, ValueTree, + ModuleIndex, Operand, Place, PlaceElem, Rvalue, Span, Type as IRType, TypeIndex, UnOp, + ValueTree, }; use ariadne::Source; use melior::helpers::{ArithBlockExt, BuiltinBlockExt, GepIndex, LlvmBlockExt}; @@ -497,7 +498,7 @@ fn compile_rvalue<'c: 'b, 'b>( Rvalue::Use(info) => compile_load_operand(ctx, block, info, locals)?, Rvalue::LogicOp(_, _) => todo!(), Rvalue::BinaryOp(op, (lhs, rhs)) => compile_binop(ctx, block, op, lhs, rhs, locals)?, - Rvalue::UnaryOp(_, _) => todo!(), + Rvalue::UnaryOp(op, lhs) => compile_unop(ctx, block, op, lhs, locals)?, Rvalue::Ref(_mutability, place) => { let mut value = locals[&place.local]; let mut local_type_idx = ctx.get_fn_body().locals[place.local].ty; @@ -999,6 +1000,55 @@ fn compile_binop<'c: 'b, 'b>( }) } +/// Compiles a unary operation. +fn compile_unop<'c: 'b, 'b>( + ctx: &'c FunctionCodegenCtx, + block: &'b Block<'c>, + op: &UnOp, + lhs: &Operand, + locals: &HashMap>, +) -> Result<(Value<'c, 'b>, TypeIndex), CodegenError> { + let (lhs, lhs_type_idx) = compile_load_operand(ctx, block, lhs, locals)?; + let location = Location::unknown(ctx.context()); + let lhs_ty = ctx.module.get_type(lhs_type_idx); + let lhs_type = compile_type(ctx.module, &lhs_ty); + + let is_float = matches!(lhs_ty, IRType::Float(_)); + + Ok(match op { + UnOp::Not => { + let k0 = block.const_int_from_type(ctx.context(), location, 0, lhs_type)?; + let value = if is_float { + block.append_op_result(arith::cmpf( + ctx.context(), + arith::CmpfPredicate::Ueq, + lhs, + k0, + location, + ))? + } else { + block.append_op_result(arith::cmpi( + ctx.context(), + arith::CmpiPredicate::Eq, + lhs, + k0, + location, + ))? + }; + (value, lhs_type_idx) + } + UnOp::Neg => { + let value = if is_float { + block.append_op_result(arith::negf(lhs, location))? + } else { + let k1 = block.const_int_from_type(ctx.context(), location, -1, lhs_type)?; + block.append_op_result(arith::muli(lhs, k1, location))? + }; + (value, lhs_type_idx) + } + }) +} + fn compile_load_operand<'c: 'b, 'b>( ctx: &'c FunctionCodegenCtx, block: &'b Block<'c>, diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 343c22e..27238a5 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -14,6 +14,7 @@ use owo_colors::OwoColorize; use std::io::Read; use std::os::unix::process::CommandExt; use std::path::Path; +use std::sync::Arc; use std::{collections::HashMap, fs::File, path::PathBuf, time::Instant}; use tracing::debug; @@ -49,6 +50,8 @@ enum Commands { Build(BuildArgs), /// Run a project or file Run(BuildArgs), + /// Test a project or file. + Test(BuildArgs), } #[derive(Args, Debug)] @@ -219,11 +222,11 @@ pub fn main() -> Result<()> { path.join("src").join("main.con"), format!( r#" -mod {} {{ - pub fn main() -> i32 {{ - return 0; - }} -}}"#, + mod {} {{ + pub fn main() -> i32 {{ + return 0; + }} + }}"#, name ), )?; @@ -232,11 +235,11 @@ mod {} {{ path.join("src").join("lib.con"), format!( r#" -mod {} {{ - pub fn hello_world() -> i32 {{ - return 0; - }} -}}"#, + mod {} {{ + pub fn hello_world() -> i32 {{ + return 0; + }} + }}"#, name ), )?; @@ -272,15 +275,77 @@ mod {} {{ handle_build(args)?; } Commands::Run(args) => { - let output = handle_build(args)?; + let output = handle_build(args)?.0; println!(); Err(std::process::Command::new(output).exec())?; } + Commands::Test(mut args) => { + args.lib = true; + let (output, tests) = handle_build(args)?; + println!(); + + let tests = Arc::new(tests); + + println!("Running {} tests", tests.len()); + + let mut passed = 0; + + let lib = Arc::new(unsafe { + libloading::os::unix::Library::new(output).expect("failed to load") + }); + + for test in tests.iter() { + print!("test {} ... ", test.symbol); + let test_fn = unsafe { + lib.get:: i32>(test.mangled_symbol.as_bytes()) + }; + + if test_fn.is_err() { + println!("{}", "err".red()); + eprintln!("Symbol not found: {:?}", test_fn); + continue; + } + + let test_fn = test_fn.unwrap(); + + let result = unsafe { (test_fn)() }; + + if result == 0 { + passed += 1; + println!("{}", "ok".green()); + } else { + println!("{}", "err".red()); + } + } + + println!(); + if !tests.is_empty() { + println!( + "test result: {}. {} passed; {} failed; ({:.2}%)", + if passed == tests.len() { + "ok".green().to_string() + } else { + "err".red().to_string() + }, + passed, + tests.len() - passed, + ((passed as f64 / tests.len() as f64) * 100.0).bold() + ); + } + + return Ok(()); + } } Ok(()) } +#[derive(Debug, Clone)] +pub struct TestInfo { + pub mangled_symbol: String, + pub symbol: String, +} + fn handle_build( BuildArgs { path, @@ -295,7 +360,7 @@ fn handle_build( lib, check, }: BuildArgs, -) -> Result { +) -> Result<(PathBuf, Vec)> { match path { // Single file compilation Some(input) => { @@ -332,7 +397,7 @@ fn handle_build( ); let start = Instant::now(); - let object = compile(&compile_args)?; + let (object, tests) = compile(&compile_args)?; if lib { link_shared_lib(&[object.clone()], &output)?; @@ -352,7 +417,7 @@ fn handle_build( if release { "release" } else { "dev" }, ); - Ok(output) + Ok((output, tests)) } // Project compilation. None => { @@ -393,7 +458,7 @@ fn handle_build( if !target_dir.exists() { std::fs::create_dir_all(&target_dir)?; } - let output = target_dir.join(config.package.name); + let mut output = target_dir.join(config.package.name); let (profile, profile_name) = if let Some(profile) = profile { ( config @@ -425,6 +490,8 @@ fn handle_build( let start = Instant::now(); + let mut tests = Vec::new(); + for file in [main_ed, lib_ed] { if file.exists() { let is_lib = file.file_stem().unwrap() == "lib"; @@ -452,13 +519,18 @@ fn handle_build( mlir, check, }; - let object = compile(&compile_args)?; + let (object, file_tests) = compile(&compile_args)?; + tests.extend(file_tests); if compile_args.library { link_shared_lib(&[object], &compile_args.output)?; } else { link_binary(&[object], &compile_args.output)?; } + + if is_lib { + output = compile_args.output; + } } } let elapsed = start.elapsed(); @@ -478,7 +550,7 @@ fn handle_build( } ); - Ok(output) + Ok((output, tests)) } } } @@ -560,7 +632,7 @@ pub fn parse_file(mut path: PathBuf, db: &dyn salsa::Database) -> Result Result { +pub fn compile(args: &CompilerArgs) -> Result<(PathBuf, Vec)> { let start_time = Instant::now(); let db = crate::driver::db::DatabaseImpl::default(); @@ -632,5 +704,14 @@ pub fn compile(args: &CompilerArgs) -> Result { let elapsed = start_time.elapsed(); tracing::debug!("Done in {:?}", elapsed); - Ok(object_path) + let mut test_names = Vec::new(); + for t in &compile_unit_ir.tests { + let f = compile_unit_ir.functions[*t].as_ref().unwrap(); + test_names.push(TestInfo { + mangled_symbol: f.name.clone(), + symbol: f.debug_name.clone().unwrap(), + }); + } + + Ok((object_path, test_names)) } diff --git a/src/ir/lowering/errors.rs b/src/ir/lowering/errors.rs index 37d5901..dfd024e 100644 --- a/src/ir/lowering/errors.rs +++ b/src/ir/lowering/errors.rs @@ -75,6 +75,12 @@ pub enum LoweringError { expected_span: Option, path: PathBuf, }, + #[error("invalid unary op on given type")] + InvalidUnaryOp { + found_span: Span, + found: String, + path: PathBuf, + }, #[error("extern function {name:?} has a body")] ExternFnWithBody { span: Span, diff --git a/src/ir/lowering/expressions.rs b/src/ir/lowering/expressions.rs index f9c20d3..532b5b4 100644 --- a/src/ir/lowering/expressions.rs +++ b/src/ir/lowering/expressions.rs @@ -4,7 +4,8 @@ use tracing::{debug, instrument}; use crate::{ ast::expressions::{ - ArithOp, BinaryOp, BitwiseOp, CmpOp, Expression, LogicOp, PathOp, PathSegment, ValueExpr, + ArithOp, BinaryOp, BitwiseOp, CmpOp, Expression, LogicOp, PathOp, PathSegment, UnaryOp, + ValueExpr, }, ir::{ ConstKind, ConstValue, FloatTy, IntTy, Local, Mutability, Operand, Place, PlaceElem, Span, @@ -45,10 +46,7 @@ pub(crate) fn lower_expression( debug!("lowering if expression"); todo!() } - Expression::UnaryOp(_, _) => { - debug!("lowering unary op"); - todo!() - } + Expression::UnaryOp(op, expr) => lower_unary_op(builder, expr, *op, type_hint)?, Expression::BinaryOp(lhs, op, rhs) => lower_binary_op(builder, lhs, *op, rhs, type_hint)?, Expression::Deref(info, deref_span) => { debug!("lowering deref"); @@ -942,6 +940,79 @@ pub(crate) fn lower_path( Ok((Place { local, projection }, type_idx, info.span)) } +pub(crate) fn lower_unary_op( + builder: &mut FnIrBuilder, + lhs: &Expression, + op: UnaryOp, + type_hint: Option, +) -> Result<(Rvalue, TypeIndex, Span), LoweringError> { + let (lhs, lhs_type_idx, lhs_span) = if type_hint.is_none() { + let ty = find_expression_type(builder, lhs)?; + + let ty = if let Some(ty) = ty { + ty + } else { + // Default to i32 if cant infer type. + // Should be ok because at other points if the i32 doesn't match the expected type + // a error will be thrown, forcing user to specify types. + debug!("can't infer type, defaulting to i32"); + builder.builder.ir.get_i32_ty() + }; + lower_expression(builder, lhs, Some(ty))? + } else { + lower_expression(builder, lhs, type_hint)? + }; + + let lhs_ty = builder.builder.get_type(lhs_type_idx).clone(); + + // We must handle the special case where you can do ptr + offset. + let is_lhs_ptr = matches!(lhs_ty, Type::Ptr(_, _)); + + if is_lhs_ptr { + return Err(LoweringError::InvalidUnaryOp { + found_span: lhs_span, + found: lhs_ty.display(&builder.builder.ir).unwrap(), + path: builder.get_file_path().clone(), + }); + } + + let lhs_local = builder.add_local(Local::temp(lhs_type_idx)); + let lhs_place = Place { + local: lhs_local, + projection: vec![], + }; + + builder.statements.push(Statement { + span: None, + kind: StatementKind::StorageLive(lhs_local), + }); + + builder.statements.push(Statement { + span: None, + kind: StatementKind::Assign(lhs_place.clone(), lhs), + }); + + let lhs = Operand::Place(lhs_place); + + Ok(match op { + UnaryOp::ArithNeg => ( + Rvalue::UnaryOp(crate::ir::UnOp::Neg, lhs), + lhs_type_idx, + lhs_span, + ), + UnaryOp::LogicalNot => ( + Rvalue::UnaryOp(crate::ir::UnOp::Not, lhs), + lhs_type_idx, + lhs_span, + ), + UnaryOp::BitwiseNot => ( + Rvalue::UnaryOp(crate::ir::UnOp::Not, lhs), + lhs_type_idx, + lhs_span, + ), + }) +} + pub(crate) fn lower_binary_op( builder: &mut FnIrBuilder, lhs: &Expression, diff --git a/src/ir/lowering/functions.rs b/src/ir/lowering/functions.rs index e4dffde..719b8ec 100644 --- a/src/ir/lowering/functions.rs +++ b/src/ir/lowering/functions.rs @@ -141,6 +141,11 @@ pub(crate) fn lower_func( } else { func.decl.name.name.clone() }, + debug_name: if !func.decl.is_extern && func.decl.name.name != "main" { + builder.get_debug_name(module_idx, &func.decl.name.name) + } else { + Some(func.decl.name.name.clone()) + }, args: args_ty.clone(), ret_ty, is_extern: func.decl.is_extern, @@ -213,6 +218,14 @@ pub(crate) fn lower_func( builder.self_ty = old_self_ty; + for attr in &func.decl.attributes { + if attr.name.as_str() == "test" { + builder.ir.tests.push(fn_id); + // TODO: check its a valid test function, i.e: no arguments, returns a i32. + break; + } + } + Ok(fn_id) } @@ -536,6 +549,11 @@ pub(crate) fn lower_func_decl( } else { func.name.name.clone() }, + debug_name: if !func.is_extern && func.name.name != "main" { + builder.get_debug_name(module_idx, &func.name.name) + } else { + Some(func.name.name.clone()) + }, args: args_ty.clone(), ret_ty, is_extern: func.is_extern, diff --git a/src/ir/lowering/lower.rs b/src/ir/lowering/lower.rs index 30bfb95..66eff34 100644 --- a/src/ir/lowering/lower.rs +++ b/src/ir/lowering/lower.rs @@ -29,6 +29,7 @@ pub fn lower_compile_units(compile_units: &[ast::CompilationUnit]) -> Result Option { + let mut name_path: Vec<&str> = Vec::new(); + + let cur_module = &self.ir.modules[module_idx]; + + for parent_id in &cur_module.parents { + let module = &self.ir.modules[*parent_id]; + name_path.push(module.name.as_ref()); + } + name_path.push(cur_module.name.as_ref()); + + name_path.push(fn_name); + + Some(name_path.join("::")) + } + /// Gets the struct index for the given StructInitExpr (+ using the current generics map in builder). /// If the given struct isn't lowered yet its gets lowered (generics). pub fn get_or_lower_for_struct_init( diff --git a/src/ir/mod.rs b/src/ir/mod.rs index 29fd9a4..a7c4832 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -43,6 +43,8 @@ pub struct IR { /// Since the `modules` field is a flat structure holding all modules regardles of depth. pub top_level_modules: Vec, pub builtin_types: HashMap, + // Test functions. + pub tests: Vec, } impl IR { @@ -113,6 +115,7 @@ pub struct Module { pub struct Function { /// The name of this function pub name: String, + pub debug_name: Option, pub args: Vec, pub ret_ty: TypeIndex, pub is_extern: bool, diff --git a/std/src/lib.con b/std/src/lib.con index 8f1aee2..f19d4fb 100644 --- a/std/src/lib.con +++ b/std/src/lib.con @@ -7,4 +7,5 @@ mod std { mod string; mod vec; mod io; + mod test; } diff --git a/std/src/libc.con b/std/src/libc.con index 7991dff..f19a9b3 100644 --- a/std/src/libc.con +++ b/std/src/libc.con @@ -7,4 +7,6 @@ mod libc { extern fn fdopen(name: i32, mode: *mut u8) -> *const u8; extern fn fdopen(name: i32, mode: *mut u8) -> *const u8; extern fn fclose(file: *const u8) -> i32; + extern fn exit(status: i32); + extern fn raise(signal: i32); } diff --git a/std/src/math.con b/std/src/math.con index e6cbfca..a1f9cd2 100644 --- a/std/src/math.con +++ b/std/src/math.con @@ -16,4 +16,40 @@ mod math { return b; } } + + import std.test.{assert_eq}; + + #[test] + fn test_min() -> i32 { + if !assert_eq::(min::(4, 2), 2, "Should give the min value") { + return 1; + } + + if !assert_eq::(min::(2, 2), 2, "Should give the min value") { + return 1; + } + + if !assert_eq::(min::(2, 4), 2, "Should give the min value") { + return 1; + } + + return 0; + } + + #[test] + fn test_max() -> i32 { + if !assert_eq::(max::(4, 2), 4, "Should give the max value") { + return 1; + } + + if !assert_eq::(max::(4, 4), 4, "Should give the max value") { + return 1; + } + + if !assert_eq::(max::(4, 5), 4, "Should give the max value") { + return 1; + } + + return 0; + } } diff --git a/std/src/mem.con b/std/src/mem.con index d63d320..c82eebb 100644 --- a/std/src/mem.con +++ b/std/src/mem.con @@ -4,4 +4,96 @@ mod mem { #[intrinsic = "alignof"] pub fn alignof() -> u64; + + import std.test.{assert_eq}; + + #[test] + fn test_size() -> i32 { + if !assert_eq::(sizeof::(), 1, "Sizeof should be correct i8") { + return 1; + } + + if !assert_eq::(sizeof::(), 2, "Sizeof should be correct i16") { + return 1; + } + + if !assert_eq::(sizeof::(), 4, "Sizeof should be correct i32") { + return 1; + } + + if !assert_eq::(sizeof::(), 8, "Sizeof should be correct i64") { + return 1; + } + + if !assert_eq::(sizeof::(), 1, "Sizeof should be correct u8") { + return 1; + } + + if !assert_eq::(sizeof::(), 2, "Sizeof should be correct u16") { + return 1; + } + + if !assert_eq::(sizeof::(), 4, "Sizeof should be correct u32") { + return 1; + } + + if !assert_eq::(sizeof::(), 8, "Sizeof should be correct u64") { + return 1; + } + + if !assert_eq::(sizeof::(), 4, "Sizeof should be correct f32") { + return 1; + } + + if !assert_eq::(sizeof::(), 8, "Sizeof should be correct f64") { + return 1; + } + + return 0; + } + + #[test] + fn test_align() -> i32 { + if !assert_eq::(alignof::(), 1, "alignof should be correct i8") { + return 1; + } + + if !assert_eq::(alignof::(), 2, "alignof should be correct i16") { + return 1; + } + + if !assert_eq::(alignof::(), 4, "alignof should be correct i32") { + return 1; + } + + if !assert_eq::(alignof::(), 8, "alignof should be correct i64") { + return 1; + } + + if !assert_eq::(alignof::(), 1, "alignof should be correct u8") { + return 1; + } + + if !assert_eq::(alignof::(), 2, "alignof should be correct u16") { + return 1; + } + + if !assert_eq::(alignof::(), 4, "alignof should be correct u32") { + return 1; + } + + if !assert_eq::(alignof::(), 8, "alignof should be correct u64") { + return 1; + } + + if !assert_eq::(alignof::(), 4, "alignof should be correct f32") { + return 1; + } + + if !assert_eq::(alignof::(), 8, "alignof should be correct f64") { + return 1; + } + + return 0; + } } diff --git a/std/src/test.con b/std/src/test.con new file mode 100644 index 0000000..dd9a7d4 --- /dev/null +++ b/std/src/test.con @@ -0,0 +1,20 @@ +mod test { + import std.io.{print}; + + pub fn assert(result: bool, message: String) { + if result == false { + std::io::print(&message); + std::libc::raise(10); // SIGUSR1 + } + } + + pub fn assert_eq(expected: T, result: T, message: String) -> bool { + if expected != result { + let msg: String = "Assert error:"; + print(&msg); + print(&message); + return false; + } + return true; + } +}