From fffacd2467e5173aef5c5c0cfc771b7015f1aecd Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Wed, 18 Feb 2026 16:35:24 -0500 Subject: [PATCH] feat: C backend module import support, Int/Float.toString, Test.assertEqualMsg The C backend can now compile programs that import user-defined modules. Module-qualified calls like `mymodule.func(args)` are resolved to prefixed C functions (e.g., `mymodule_func_lux`), with full support for transitive imports and effect-passing. Also adds Int.toString/Float.toString to type system, interpreter, and C backend, and Test.assertEqualMsg for labeled test assertions. Co-Authored-By: Claude Opus 4.6 --- src/codegen/c_backend.rs | 506 ++++++++++++++++++++++++++++++++++++++- src/interpreter.rs | 56 +++++ src/main.rs | 13 +- src/modules.rs | 5 + src/types.rs | 27 +++ 5 files changed, 593 insertions(+), 14 deletions(-) diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index cb374b9..035ca52 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -43,6 +43,7 @@ //! - More effects (File, Http, etc.) use crate::ast::*; +use crate::modules::Module; use std::collections::{HashSet, HashMap}; use std::fmt::Write; @@ -139,6 +140,12 @@ pub struct CBackend { function_behaviors: HashMap, /// Whether to enable behavioral type optimizations enable_behavioral_optimizations: bool, + /// Mapping from (module_name, func_name) to mangled C function name for imported modules + module_functions: HashMap<(String, String), String>, + /// Set of module names that have been imported (for resolving calls) + imported_modules: HashSet, + /// Variable name renames: Lux name → C variable name (for let binding name mangling) + var_renames: HashMap, } impl CBackend { @@ -167,6 +174,9 @@ impl CBackend { var_types: HashMap::new(), function_behaviors: HashMap::new(), enable_behavioral_optimizations: true, + module_functions: HashMap::new(), + imported_modules: HashSet::new(), + var_renames: HashMap::new(), } } @@ -191,7 +201,7 @@ impl CBackend { } /// Generate C code from a Lux program - pub fn generate(&mut self, program: &Program) -> Result { + pub fn generate(&mut self, program: &Program, modules: &HashMap) -> Result { self.output.clear(); self.closures.clear(); self.emit_prelude(); @@ -208,6 +218,9 @@ impl CBackend { self.variant_field_types.insert(("Result".to_string(), "Ok".to_string()), vec!["void*".to_string()]); self.variant_field_types.insert(("Result".to_string(), "Err".to_string()), vec!["void*".to_string()]); + // Process imported modules before the main program + self.process_imported_modules(program, modules)?; + // First pass: collect all function names, types, and effects for decl in &program.declarations { match decl { @@ -240,19 +253,24 @@ impl CBackend { } } - // Emit type definitions + // Emit type definitions (modules first, then main program) + self.emit_module_type_definitions(modules, program)?; self.emit_type_definitions(program)?; // Emit ADT drop function (after type definitions so we know the ADT structure) self.emit_adt_drop_function(); - // Emit forward declarations for regular functions + // Emit forward declarations for module functions, then main program + self.emit_module_forward_declarations()?; self.emit_forward_declarations(program)?; // Generate function bodies to a temporary buffer // This collects closures that need to be emitted let saved_output = std::mem::take(&mut self.output); + // Emit module function bodies first + self.emit_module_function_bodies(modules, program)?; + for decl in &program.declarations { match decl { Declaration::Function(f) => { @@ -282,6 +300,328 @@ impl CBackend { Ok(self.output.clone()) } + /// Process imported modules: collect function names, types, and mappings + fn process_imported_modules(&mut self, program: &Program, modules: &HashMap) -> Result<(), CGenError> { + // Build mapping from import alias → module path + let mut import_aliases: HashMap = HashMap::new(); + for import in &program.imports { + let module_path = import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let alias = if let Some(ref a) = import.alias { + a.name.clone() + } else { + import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| module_path.clone()) + }; + import_aliases.insert(alias.clone(), module_path.clone()); + self.imported_modules.insert(alias); + } + + // Process each imported module (and transitive imports) + let mut processed = HashSet::new(); + for import in &program.imports { + let module_path = import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let alias = if let Some(ref a) = import.alias { + a.name.clone() + } else { + import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| module_path.clone()) + }; + self.process_single_module(&alias, &module_path, modules, &mut processed)?; + } + + Ok(()) + } + + /// Process a single module and its transitive imports + fn process_single_module( + &mut self, + alias: &str, + module_path: &str, + modules: &HashMap, + processed: &mut HashSet, + ) -> Result<(), CGenError> { + if processed.contains(module_path) { + return Ok(()); + } + processed.insert(module_path.to_string()); + + let module = match modules.get(module_path) { + Some(m) => m, + None => return Ok(()), // Module not found - might be a built-in + }; + + // Process transitive imports first + for sub_import in &module.program.imports { + let sub_path = sub_import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let sub_alias = sub_import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| sub_path.clone()); + self.process_single_module(&sub_alias, &sub_path, modules, processed)?; + } + + // Collect types from the module + for decl in &module.program.declarations { + if let Declaration::Type(t) = decl { + self.collect_type(t)?; + } + } + + // Collect functions from the module + for decl in &module.program.declarations { + if let Declaration::Function(f) = decl { + if f.visibility == Visibility::Public || module.exports.contains(&f.name.name) { + let mangled = format!("{}_{}_lux", alias, f.name.name); + + self.functions.insert(mangled.clone()); + self.module_functions.insert( + (alias.to_string(), f.name.name.clone()), + mangled.clone(), + ); + + // Store return type + if let Ok(ret_type) = self.type_expr_to_c(&f.return_type) { + self.function_return_types.insert(mangled.clone(), ret_type); + } + + // Store param types + let param_types: Vec = f.params.iter() + .filter_map(|p| self.type_expr_to_c(&p.typ).ok()) + .collect(); + self.function_param_types.insert(mangled.clone(), param_types); + + // Check for closures + if matches!(&f.return_type, TypeExpr::Function { .. }) { + self.closure_returning_functions.insert(mangled.clone()); + } + + // Check for effects + if !f.effects.is_empty() { + self.effectful_functions.insert(mangled.clone()); + } + + self.collect_behavioral_properties(f); + } + } + } + + Ok(()) + } + + /// Emit type definitions from imported modules + fn emit_module_type_definitions(&mut self, modules: &HashMap, program: &Program) -> Result<(), CGenError> { + let mut processed = HashSet::new(); + for import in &program.imports { + let module_path = import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + self.emit_module_types_recursive(&module_path, modules, &mut processed)?; + } + Ok(()) + } + + fn emit_module_types_recursive( + &mut self, + module_path: &str, + modules: &HashMap, + processed: &mut HashSet, + ) -> Result<(), CGenError> { + if processed.contains(module_path) { + return Ok(()); + } + processed.insert(module_path.to_string()); + + let module = match modules.get(module_path) { + Some(m) => m, + None => return Ok(()), + }; + + // Process transitive imports first + for sub_import in &module.program.imports { + let sub_path = sub_import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + self.emit_module_types_recursive(&sub_path, modules, processed)?; + } + + for decl in &module.program.declarations { + if let Declaration::Type(t) = decl { + self.emit_type_def(t)?; + } + } + + Ok(()) + } + + /// Emit forward declarations for module functions + fn emit_module_forward_declarations(&mut self) -> Result<(), CGenError> { + if self.module_functions.is_empty() { + return Ok(()); + } + self.writeln("// === Imported Module Forward Declarations ==="); + // Forward declarations are emitted from stored info + // We iterate module_functions and use function_return_types/function_param_types + let entries: Vec<_> = self.module_functions.values().cloned().collect(); + for mangled in &entries { + let ret_type = self.function_return_types.get(mangled) + .cloned() + .unwrap_or_else(|| "LuxInt".to_string()); + let param_types = self.function_param_types.get(mangled) + .cloned() + .unwrap_or_default(); + let is_effectful = self.effectful_functions.contains(mangled); + + let params_str = if param_types.is_empty() && !is_effectful { + "void".to_string() + } else { + let mut parts = Vec::new(); + if is_effectful { + parts.push("LuxEvidence* ev".to_string()); + } + for (i, pt) in param_types.iter().enumerate() { + parts.push(format!("{} param{}", pt, i)); + } + if parts.is_empty() { + "void".to_string() + } else { + parts.join(", ") + } + }; + + self.writeln(&format!("{} {}({});", ret_type, mangled, params_str)); + } + self.writeln(""); + Ok(()) + } + + /// Emit function bodies for imported modules + fn emit_module_function_bodies( + &mut self, + modules: &HashMap, + program: &Program, + ) -> Result<(), CGenError> { + if self.module_functions.is_empty() { + return Ok(()); + } + + self.writeln("// === Imported Module Function Bodies ==="); + self.writeln(""); + + let mut processed = HashSet::new(); + for import in &program.imports { + let module_path = import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let alias = if let Some(ref a) = import.alias { + a.name.clone() + } else { + import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| module_path.clone()) + }; + self.emit_module_bodies_recursive(&alias, &module_path, modules, &mut processed)?; + } + + Ok(()) + } + + fn emit_module_bodies_recursive( + &mut self, + alias: &str, + module_path: &str, + modules: &HashMap, + processed: &mut HashSet, + ) -> Result<(), CGenError> { + if processed.contains(module_path) { + return Ok(()); + } + processed.insert(module_path.to_string()); + + let module = match modules.get(module_path) { + Some(m) => m.clone(), + None => return Ok(()), + }; + + // Process transitive imports first + for sub_import in &module.program.imports { + let sub_path = sub_import.path.segments.iter() + .map(|s| s.name.as_str()) + .collect::>() + .join("/"); + let sub_alias = sub_import.path.segments.last() + .map(|s| s.name.clone()) + .unwrap_or_else(|| sub_path.clone()); + self.emit_module_bodies_recursive(&sub_alias, &sub_path, modules, processed)?; + } + + for decl in &module.program.declarations { + if let Declaration::Function(f) = decl { + if f.visibility == Visibility::Public || module.exports.contains(&f.name.name) { + let mangled = format!("{}_{}_lux", alias, f.name.name); + self.emit_function_with_name(f, &mangled)?; + } + } + } + + Ok(()) + } + + /// Emit a function body with a custom mangled name + fn emit_function_with_name(&mut self, func: &FunctionDecl, mangled_name: &str) -> Result<(), CGenError> { + let ret_type = self.type_expr_to_c(&func.return_type)?; + let params = self.emit_params(&func.params)?; + + let is_effectful = !func.effects.is_empty(); + + let full_params = if is_effectful { + if params == "void" { + "LuxEvidence* ev".to_string() + } else { + format!("LuxEvidence* ev, {}", params) + } + } else { + params + }; + + self.writeln(&format!("{} {}({}) {{", ret_type, mangled_name, full_params)); + self.indent += 1; + + let old_has_evidence = self.has_evidence; + if is_effectful { + self.has_evidence = true; + } + + let body_result = self.emit_expr(&func.body)?; + + if ret_type != "void" && ret_type != "LuxUnit" { + self.writeln(&format!("return {};", body_result)); + } else if ret_type == "LuxUnit" { + self.writeln(&format!("(void){};", body_result)); + self.writeln("return 0;"); + } + + self.has_evidence = old_has_evidence; + + self.indent -= 1; + self.writeln("}"); + self.writeln(""); + + Ok(()) + } + /// Emit all collected closure definitions fn emit_closures(&mut self) -> Result<(), CGenError> { if self.closures.is_empty() { @@ -363,6 +703,8 @@ impl CBackend { let escaped = self.escape_c_keyword(&ident.name); if captured.contains(ident.name.as_str()) { Ok(format!("env->{}", escaped)) + } else if let Some(renamed) = self.var_renames.get(&ident.name) { + Ok(renamed.clone()) } else if self.functions.contains(&ident.name) { Ok(self.mangle_name(&ident.name)) } else { @@ -675,6 +1017,15 @@ impl CBackend { self.writeln(" return result;"); self.writeln("}"); self.writeln(""); + self.writeln("static LuxString lux_float_to_string(LuxFloat f) {"); + self.writeln(" char buffer[64];"); + self.writeln(" snprintf(buffer, sizeof(buffer), \"%g\", f);"); + self.writeln(" size_t len = strlen(buffer);"); + self.writeln(" LuxString result = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);"); + self.writeln(" memcpy(result, buffer, len + 1);"); + self.writeln(" return result;"); + self.writeln("}"); + self.writeln(""); self.writeln("static LuxBool lux_string_eq(LuxString a, LuxString b) {"); self.writeln(" return strcmp(a, b) == 0;"); self.writeln("}"); @@ -2392,6 +2743,9 @@ impl CBackend { // This is a constructor - emit struct literal let variant_name = &ident.name; Ok(format!("({}){{{}_TAG_{}}}", type_name, type_name, variant_name.to_uppercase())) + } else if let Some(renamed) = self.var_renames.get(&ident.name) { + // Variable has been renamed by a let binding + Ok(renamed.clone()) } else if self.functions.contains(&ident.name) { // Function used as a value — wrap in a closure struct let mangled = self.mangle_name(&ident.name); @@ -2626,19 +2980,68 @@ impl CBackend { self.writeln(&format!("{} {} = {};", var_type, var_name, val)); - // Substitute the name in the body - // For now, assume the variable is directly usable - let body_result = self.emit_expr_with_substitution(body, &name.name, &var_name)?; + // Register the variable rename so nested expressions can find it + let old_rename = self.var_renames.insert(name.name.clone(), var_name.clone()); + self.var_types.insert(var_name.clone(), var_type); + + let body_result = self.emit_expr(body)?; + + // Restore previous rename (or remove) + if let Some(prev) = old_rename { + self.var_renames.insert(name.name.clone(), prev); + } else { + self.var_renames.remove(&name.name); + } + Ok(body_result) } Expr::Call { func, args, .. } => { - // Check for List module calls first (List.map, List.filter, etc.) + // Check for module calls: List, String, Int, Float, and user-defined modules if let Expr::Field { object, field, .. } = func.as_ref() { if let Expr::Var(module_name) = object.as_ref() { if module_name.name == "List" { return self.emit_list_operation(&field.name, args); } + // Int module + if module_name.name == "Int" && field.name == "toString" { + let arg = self.emit_expr(&args[0])?; + let temp = format!("_int_to_string_{}", self.fresh_name()); + self.writeln(&format!("LuxString {} = lux_int_to_string({});", temp, arg)); + self.register_rc_var(&temp, "LuxString"); + return Ok(temp); + } + // Float module + if module_name.name == "Float" && field.name == "toString" { + let arg = self.emit_expr(&args[0])?; + let temp = format!("_float_to_string_{}", self.fresh_name()); + self.writeln(&format!("LuxString {} = lux_float_to_string({});", temp, arg)); + self.register_rc_var(&temp, "LuxString"); + return Ok(temp); + } + // Check for user-defined module function + let key = (module_name.name.clone(), field.name.clone()); + if let Some(c_name) = self.module_functions.get(&key).cloned() { + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); + let args_str = arg_strs?.join(", "); + let is_effectful = self.effectful_functions.contains(&c_name); + let call_expr = if is_effectful && self.has_evidence { + if args_str.is_empty() { + format!("{}(ev)", c_name) + } else { + format!("{}(ev, {})", c_name, args_str) + } + } else if is_effectful { + if args_str.is_empty() { + format!("{}(&default_evidence)", c_name) + } else { + format!("{}(&default_evidence, {})", c_name, args_str) + } + } else { + format!("{}({})", c_name, args_str) + }; + return Ok(call_expr); + } } } @@ -2951,6 +3354,34 @@ impl CBackend { return self.emit_list_operation(&operation.name, args); } + // Int module + if effect.name == "Int" { + match operation.name.as_str() { + "toString" => { + let arg = self.emit_expr(&args[0])?; + let temp = format!("_int_to_string_{}", self.fresh_name()); + self.writeln(&format!("LuxString {} = lux_int_to_string({});", temp, arg)); + self.register_rc_var(&temp, "LuxString"); + return Ok(temp); + } + _ => {} + } + } + + // Float module + if effect.name == "Float" { + match operation.name.as_str() { + "toString" => { + let arg = self.emit_expr(&args[0])?; + let temp = format!("_float_to_string_{}", self.fresh_name()); + self.writeln(&format!("LuxString {} = lux_float_to_string({});", temp, arg)); + self.register_rc_var(&temp, "LuxString"); + return Ok(temp); + } + _ => {} + } + } + // Built-in Console effect if effect.name == "Console" { if operation.name == "print" { @@ -3370,6 +3801,32 @@ impl CBackend { } } + // Check for user-defined module function (via EffectOp path) + { + let key = (effect.name.clone(), operation.name.clone()); + if let Some(c_name) = self.module_functions.get(&key).cloned() { + let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); + let args_str = arg_strs?.join(", "); + let is_effectful = self.effectful_functions.contains(&c_name); + let call_expr = if is_effectful && self.has_evidence { + if args_str.is_empty() { + format!("{}(ev)", c_name) + } else { + format!("{}(ev, {})", c_name, args_str) + } + } else if is_effectful { + if args_str.is_empty() { + format!("{}(&default_evidence)", c_name) + } else { + format!("{}(&default_evidence, {})", c_name, args_str) + } + } else { + format!("{}({})", c_name, args_str) + }; + return Ok(call_expr); + } + } + // For other effects, emit generic evidence-passing call let arg_strs: Result, _> = args.iter().map(|a| self.emit_expr(a)).collect(); if self.has_evidence { @@ -4282,7 +4739,15 @@ impl CBackend { "parse" => return Some("Option".to_string()), _ => return Some("LuxFloat".to_string()), }, - _ => {} + _ => { + // Check user-defined module functions + let key = (module.name.clone(), field.name.clone()); + if let Some(c_name) = self.module_functions.get(&key) { + if let Some(ret_type) = self.function_return_types.get(c_name) { + return Some(ret_type.clone()); + } + } + } } } } @@ -4319,6 +4784,20 @@ impl CBackend { } Expr::List { .. } => Some("LuxList*".to_string()), Expr::EffectOp { effect, operation, args, .. } => { + // Int module + if effect.name == "Int" { + match operation.name.as_str() { + "toString" => return Some("LuxString".to_string()), + _ => return None, + } + } + // Float module + if effect.name == "Float" { + match operation.name.as_str() { + "toString" => return Some("LuxString".to_string()), + _ => return None, + } + } // List operations have known return types if effect.name == "List" { match operation.name.as_str() { @@ -4392,7 +4871,13 @@ impl CBackend { _ => None, } } else { - None + // Check user-defined module functions + let key = (effect.name.clone(), operation.name.clone()); + if let Some(c_name) = self.module_functions.get(&key) { + self.function_return_types.get(c_name).cloned() + } else { + None + } } } Expr::Match { arms, .. } => { @@ -5612,7 +6097,8 @@ mod tests { fn generate(source: &str) -> Result { let program = Parser::parse_source(source).expect("Parse error"); let mut backend = CBackend::new(); - backend.generate(&program) + let modules = HashMap::new(); + backend.generate(&program, &modules) } #[test] diff --git a/src/interpreter.rs b/src/interpreter.rs index a0cf0ae..9df37c3 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -95,6 +95,10 @@ pub enum BuiltinFn { StringLastIndexOf, StringRepeat, + // Int/Float operations + IntToString, + FloatToString, + // JSON operations JsonParse, JsonStringify, @@ -1071,6 +1075,18 @@ impl Interpreter { ])); env.define("Math", math_module); + // Int module + let int_module = Value::Record(HashMap::from([ + ("toString".to_string(), Value::Builtin(BuiltinFn::IntToString)), + ])); + env.define("Int", int_module); + + // Float module + let float_module = Value::Record(HashMap::from([ + ("toString".to_string(), Value::Builtin(BuiltinFn::FloatToString)), + ])); + env.define("Float", float_module); + // JSON module let json_module = Value::Record(HashMap::from([ ("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)), @@ -2251,6 +2267,26 @@ impl Interpreter { Ok(EvalResult::Value(Value::String(result))) } + BuiltinFn::IntToString => { + if args.len() != 1 { + return Err(err("Int.toString requires 1 argument")); + } + match &args[0] { + Value::Int(n) => Ok(EvalResult::Value(Value::String(format!("{}", n)))), + v => Ok(EvalResult::Value(Value::String(format!("{}", v)))), + } + } + + BuiltinFn::FloatToString => { + if args.len() != 1 { + return Err(err("Float.toString requires 1 argument")); + } + match &args[0] { + Value::Float(f) => Ok(EvalResult::Value(Value::String(format!("{}", f)))), + v => Ok(EvalResult::Value(Value::String(format!("{}", v)))), + } + } + BuiltinFn::TypeOf => { if args.len() != 1 { return Err(err("typeOf requires 1 argument")); @@ -3856,6 +3892,26 @@ impl Interpreter { } Ok(Value::Unit) } + ("Test", "assertEqualMsg") => { + let expected = request.args.first().cloned().unwrap_or(Value::Unit); + let actual = request.args.get(1).cloned().unwrap_or(Value::Unit); + let label = match request.args.get(2) { + Some(Value::String(s)) => s.clone(), + _ => "Values not equal".to_string(), + }; + + if Value::values_equal(&expected, &actual) { + self.test_results.borrow_mut().passed += 1; + } else { + self.test_results.borrow_mut().failed += 1; + self.test_results.borrow_mut().failures.push(TestFailure { + message: label, + expected: Some(format!("{}", expected)), + actual: Some(format!("{}", actual)), + }); + } + Ok(Value::Unit) + } ("Test", "assertNotEqual") => { let a = request.args.first().cloned().unwrap_or(Value::Unit); let b = request.args.get(1).cloned().unwrap_or(Value::Unit); diff --git a/src/main.rs b/src/main.rs index 9090f88..d8c400d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,9 +174,14 @@ fn main() { .and_then(|s| s.parse::().ok()) .unwrap_or(8080); - let dir = args.get(2) - .filter(|a| !a.starts_with('-')) - .map(|s| s.as_str()) + let port_value_idx = args.iter() + .position(|a| a == "--port" || a == "-p") + .map(|i| i + 1); + let dir = args.iter().enumerate() + .skip(2) + .filter(|(i, a)| !a.starts_with('-') && Some(*i) != port_value_idx) + .map(|(_, a)| a.as_str()) + .next() .unwrap_or("."); serve_static_files(dir, port); @@ -842,7 +847,7 @@ fn compile_to_c(path: &str, output_path: Option<&str>, run_after: bool, emit_c: // Generate C code let mut backend = CBackend::new(); - let c_code = match backend.generate(&program) { + let c_code = match backend.generate(&program, loader.module_cache()) { Ok(code) => code, Err(e) => { eprintln!("{} C codegen: {}", c(colors::RED, "error:"), e); diff --git a/src/modules.rs b/src/modules.rs index 9d40bd8..2956b4b 100644 --- a/src/modules.rs +++ b/src/modules.rs @@ -305,6 +305,11 @@ impl ModuleLoader { self.cache.iter() } + /// Get the module cache (for passing to C backend) + pub fn module_cache(&self) -> &HashMap { + &self.cache + } + /// Clear the module cache pub fn clear_cache(&mut self) { self.cache.clear(); diff --git a/src/types.rs b/src/types.rs index 7c7af8b..2d14315 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1146,6 +1146,15 @@ impl TypeEnv { ], return_type: Type::Unit, }, + EffectOpDef { + name: "assertEqualMsg".to_string(), + params: vec![ + ("expected".to_string(), Type::Var(0)), + ("actual".to_string(), Type::Var(0)), + ("label".to_string(), Type::String), + ], + return_type: Type::Unit, + }, EffectOpDef { name: "assertNotEqual".to_string(), params: vec![ @@ -1881,6 +1890,24 @@ impl TypeEnv { ]); env.bind("Math", TypeScheme::mono(math_module_type)); + // Int module + let int_module_type = Type::Record(vec![ + ( + "toString".to_string(), + Type::function(vec![Type::Int], Type::String), + ), + ]); + env.bind("Int", TypeScheme::mono(int_module_type)); + + // Float module + let float_module_type = Type::Record(vec![ + ( + "toString".to_string(), + Type::function(vec![Type::Float], Type::String), + ), + ]); + env.bind("Float", TypeScheme::mono(float_module_type)); + env }