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 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 16:35:24 -05:00
parent 2ae2c132e5
commit fffacd2467
5 changed files with 593 additions and 14 deletions

View File

@@ -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<String, FunctionBehavior>,
/// 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<String>,
/// Variable name renames: Lux name → C variable name (for let binding name mangling)
var_renames: HashMap<String, String>,
}
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<String, CGenError> {
pub fn generate(&mut self, program: &Program, modules: &HashMap<String, Module>) -> Result<String, CGenError> {
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<String, Module>) -> Result<(), CGenError> {
// Build mapping from import alias → module path
let mut import_aliases: HashMap<String, String> = HashMap::new();
for import in &program.imports {
let module_path = import.path.segments.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.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::<Vec<_>>()
.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<String, Module>,
processed: &mut HashSet<String>,
) -> 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::<Vec<_>>()
.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<String> = 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<String, Module>, 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::<Vec<_>>()
.join("/");
self.emit_module_types_recursive(&module_path, modules, &mut processed)?;
}
Ok(())
}
fn emit_module_types_recursive(
&mut self,
module_path: &str,
modules: &HashMap<String, Module>,
processed: &mut HashSet<String>,
) -> 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::<Vec<_>>()
.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<String, Module>,
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::<Vec<_>>()
.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<String, Module>,
processed: &mut HashSet<String>,
) -> 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::<Vec<_>>()
.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<Vec<_>, _> = 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<Vec<_>, _> = 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<Vec<_>, _> = 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() {
@@ -4391,10 +4870,16 @@ impl CBackend {
"parseInt" | "parseFloat" => Some("Option".to_string()),
_ => None,
}
} else {
// 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, .. } => {
// Type of match is the type of its arms
Some(self.infer_match_result_type(arms))
@@ -5612,7 +6097,8 @@ mod tests {
fn generate(source: &str) -> Result<String, CGenError> {
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]

View File

@@ -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);

View File

@@ -174,9 +174,14 @@ fn main() {
.and_then(|s| s.parse::<u16>().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);

View File

@@ -305,6 +305,11 @@ impl ModuleLoader {
self.cache.iter()
}
/// Get the module cache (for passing to C backend)
pub fn module_cache(&self) -> &HashMap<String, Module> {
&self.cache
}
/// Clear the module cache
pub fn clear_cache(&mut self) {
self.cache.clear();

View File

@@ -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
}