fix: C backend String functions, record type aliases, docs cleanup

- Add String.fromChar, chars, substring, toUpper, toLower, replace,
  startsWith, endsWith, join to C backend
- Fix record type alias unification by adding expand_type_alias and
  unify_with_env functions
- Update docs to reflect current implementation status
- Clean up outdated roadmap items and fix inconsistencies
- Add comprehensive language comparison document

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 01:06:20 -05:00
parent ba3b713f8c
commit 33b4f57faf
11 changed files with 1694 additions and 571 deletions

View File

@@ -1247,6 +1247,125 @@ impl CBackend {
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString lux_string_from_char(char c) {");
self.writeln(" LuxString result = (LuxString)lux_rc_alloc(2, LUX_TAG_STRING);");
self.writeln(" if (!result) return \"\";");
self.writeln(" result[0] = c;");
self.writeln(" result[1] = '\\0';");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxList* lux_string_chars(LuxString s) {");
self.writeln(" size_t len = s ? strlen(s) : 0;");
self.writeln(" LuxList* list = lux_list_new(len);");
self.writeln(" for (size_t i = 0; i < len; i++) {");
self.writeln(" LuxString ch = lux_string_from_char(s[i]);");
self.writeln(" lux_list_push(list, (void*)ch);");
self.writeln(" }");
self.writeln(" return list;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString lux_string_substring(LuxString s, LuxInt start, LuxInt len) {");
self.writeln(" if (!s) return \"\";");
self.writeln(" size_t slen = strlen(s);");
self.writeln(" if (start < 0) start = 0;");
self.writeln(" if ((size_t)start >= slen) return \"\";");
self.writeln(" if (len < 0) len = 0;");
self.writeln(" if ((size_t)(start + len) > slen) len = slen - start;");
self.writeln(" LuxString result = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" if (!result) return \"\";");
self.writeln(" strncpy(result, s + start, len);");
self.writeln(" result[len] = '\\0';");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString lux_string_to_upper(LuxString s) {");
self.writeln(" if (!s) return \"\";");
self.writeln(" size_t len = strlen(s);");
self.writeln(" LuxString result = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" if (!result) return \"\";");
self.writeln(" for (size_t i = 0; i < len; i++) {");
self.writeln(" result[i] = (s[i] >= 'a' && s[i] <= 'z') ? s[i] - 32 : s[i];");
self.writeln(" }");
self.writeln(" result[len] = '\\0';");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString lux_string_to_lower(LuxString s) {");
self.writeln(" if (!s) return \"\";");
self.writeln(" size_t len = strlen(s);");
self.writeln(" LuxString result = (LuxString)lux_rc_alloc(len + 1, LUX_TAG_STRING);");
self.writeln(" if (!result) return \"\";");
self.writeln(" for (size_t i = 0; i < len; i++) {");
self.writeln(" result[i] = (s[i] >= 'A' && s[i] <= 'Z') ? s[i] + 32 : s[i];");
self.writeln(" }");
self.writeln(" result[len] = '\\0';");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString lux_string_replace(LuxString s, LuxString from, LuxString to) {");
self.writeln(" if (!s || !from || !*from) return s ? lux_string_dup(s) : \"\";");
self.writeln(" if (!to) to = \"\";");
self.writeln(" size_t from_len = strlen(from);");
self.writeln(" size_t to_len = strlen(to);");
self.writeln(" size_t count = 0;");
self.writeln(" const char* p = s;");
self.writeln(" while ((p = strstr(p, from)) != NULL) { count++; p += from_len; }");
self.writeln(" size_t new_len = strlen(s) + count * (to_len - from_len);");
self.writeln(" LuxString result = (LuxString)lux_rc_alloc(new_len + 1, LUX_TAG_STRING);");
self.writeln(" if (!result) return \"\";");
self.writeln(" char* out = result;");
self.writeln(" p = s;");
self.writeln(" const char* found;");
self.writeln(" while ((found = strstr(p, from)) != NULL) {");
self.writeln(" size_t prefix_len = found - p;");
self.writeln(" memcpy(out, p, prefix_len);");
self.writeln(" out += prefix_len;");
self.writeln(" memcpy(out, to, to_len);");
self.writeln(" out += to_len;");
self.writeln(" p = found + from_len;");
self.writeln(" }");
self.writeln(" strcpy(out, p);");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool lux_string_starts_with(LuxString s, LuxString prefix) {");
self.writeln(" if (!s || !prefix) return 0;");
self.writeln(" size_t prefix_len = strlen(prefix);");
self.writeln(" if (strlen(s) < prefix_len) return 0;");
self.writeln(" return strncmp(s, prefix, prefix_len) == 0;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxBool lux_string_ends_with(LuxString s, LuxString suffix) {");
self.writeln(" if (!s || !suffix) return 0;");
self.writeln(" size_t s_len = strlen(s);");
self.writeln(" size_t suffix_len = strlen(suffix);");
self.writeln(" if (s_len < suffix_len) return 0;");
self.writeln(" return strcmp(s + s_len - suffix_len, suffix) == 0;");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString lux_string_join(LuxList* list, LuxString sep) {");
self.writeln(" if (!list || list->length == 0) return \"\";");
self.writeln(" if (!sep) sep = \"\";");
self.writeln(" size_t sep_len = strlen(sep);");
self.writeln(" size_t total_len = 0;");
self.writeln(" for (int64_t i = 0; i < list->length; i++) {");
self.writeln(" LuxString elem = (LuxString)list->elements[i];");
self.writeln(" if (elem) total_len += strlen(elem);");
self.writeln(" if (i > 0) total_len += sep_len;");
self.writeln(" }");
self.writeln(" LuxString result = (LuxString)lux_rc_alloc(total_len + 1, LUX_TAG_STRING);");
self.writeln(" if (!result) return \"\";");
self.writeln(" char* out = result;");
self.writeln(" for (int64_t i = 0; i < list->length; i++) {");
self.writeln(" if (i > 0) { strcpy(out, sep); out += sep_len; }");
self.writeln(" LuxString elem = (LuxString)list->elements[i];");
self.writeln(" if (elem) { strcpy(out, elem); out += strlen(elem); }");
self.writeln(" }");
self.writeln(" *out = '\\0';");
self.writeln(" return result;");
self.writeln("}");
self.writeln("");
self.writeln("// Default evidence with built-in handlers");
self.writeln("static LuxEvidence default_evidence = {");
self.writeln(" .console = &default_console_handler,");
@@ -2863,6 +2982,72 @@ impl CBackend {
let s = self.emit_expr(&args[0])?;
return Ok(format!("lux_string_parseFloat({})", s));
}
"fromChar" => {
let c = self.emit_expr(&args[0])?;
// Create temp variable and track for cleanup (returns RC-managed string)
let temp = format!("_fromchar_{}", self.fresh_name());
self.writeln(&format!("LuxString {} = lux_string_from_char({});", temp, c));
self.register_rc_var(&temp, "LuxString");
return Ok(temp);
}
"chars" => {
let s = self.emit_expr(&args[0])?;
// Create temp variable and track for cleanup (returns RC-managed list)
let temp = format!("_chars_{}", self.fresh_name());
self.writeln(&format!("LuxList* {} = lux_string_chars({});", temp, s));
self.register_rc_var(&temp, "LuxList*");
return Ok(temp);
}
"substring" => {
let s = self.emit_expr(&args[0])?;
let start = self.emit_expr(&args[1])?;
let len = self.emit_expr(&args[2])?;
let temp = format!("_substr_{}", self.fresh_name());
self.writeln(&format!("LuxString {} = lux_string_substring({}, {}, {});", temp, s, start, len));
self.register_rc_var(&temp, "LuxString");
return Ok(temp);
}
"toUpper" => {
let s = self.emit_expr(&args[0])?;
let temp = format!("_upper_{}", self.fresh_name());
self.writeln(&format!("LuxString {} = lux_string_to_upper({});", temp, s));
self.register_rc_var(&temp, "LuxString");
return Ok(temp);
}
"toLower" => {
let s = self.emit_expr(&args[0])?;
let temp = format!("_lower_{}", self.fresh_name());
self.writeln(&format!("LuxString {} = lux_string_to_lower({});", temp, s));
self.register_rc_var(&temp, "LuxString");
return Ok(temp);
}
"replace" => {
let s = self.emit_expr(&args[0])?;
let from = self.emit_expr(&args[1])?;
let to = self.emit_expr(&args[2])?;
let temp = format!("_replace_{}", self.fresh_name());
self.writeln(&format!("LuxString {} = lux_string_replace({}, {}, {});", temp, s, from, to));
self.register_rc_var(&temp, "LuxString");
return Ok(temp);
}
"startsWith" => {
let s = self.emit_expr(&args[0])?;
let prefix = self.emit_expr(&args[1])?;
return Ok(format!("lux_string_starts_with({}, {})", s, prefix));
}
"endsWith" => {
let s = self.emit_expr(&args[0])?;
let suffix = self.emit_expr(&args[1])?;
return Ok(format!("lux_string_ends_with({}, {})", s, suffix));
}
"join" => {
let list = self.emit_expr(&args[0])?;
let sep = self.emit_expr(&args[1])?;
let temp = format!("_join_{}", self.fresh_name());
self.writeln(&format!("LuxString {} = lux_string_join({}, {});", temp, list, sep));
self.register_rc_var(&temp, "LuxString");
return Ok(temp);
}
_ => {}
}
}
@@ -3876,6 +4061,8 @@ impl CBackend {
self.writeln("");
// Execute top-level let bindings with run expressions
// Track if main was already called via a run expression
let mut main_called_via_run = false;
for decl in &program.declarations {
if let Declaration::Let(let_decl) = decl {
if matches!(&let_decl.value, Expr::Run { .. }) {
@@ -3883,6 +4070,10 @@ impl CBackend {
if let Expr::Call { func, .. } = expr.as_ref() {
if let Expr::Var(fn_name) = func.as_ref() {
let mangled = self.mangle_name(&fn_name.name);
// Track if this is a call to main
if fn_name.name == "main" {
main_called_via_run = true;
}
// Pass default evidence if function uses effects
if self.effectful_functions.contains(&fn_name.name) {
self.writeln(&format!("{}(&default_evidence);", mangled));
@@ -3896,8 +4087,8 @@ impl CBackend {
}
}
// If there's a main function, call it
if has_main {
// If there's a main function and it wasn't already called via run, call it
if has_main && !main_called_via_run {
// Check if main uses effects (Console typically)
if self.effectful_functions.contains("main") {
self.writeln("main_lux(&default_evidence);");

View File

@@ -12,9 +12,9 @@ use crate::ast::{
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
use crate::modules::ModuleLoader;
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange};
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange, AutoMigration};
use crate::types::{
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, Property, PropertySet,
self, unify, unify_with_env, EffectDef, EffectOpDef, EffectSet, HandlerDef, Property, PropertySet,
TraitBoundDef, TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef,
VariantFieldsDef, VersionInfo,
};
@@ -97,6 +97,32 @@ fn categorize_type_error(message: &str) -> (String, Vec<String>) {
"Invalid Recursion".to_string(),
vec!["Check that recursive calls have proper base cases.".to_string()],
)
} else if message_lower.contains("unknown effect") {
(
"Unknown Effect".to_string(),
vec![
"Make sure the effect is spelled correctly.".to_string(),
"Built-in effects: Console, File, Process, Http, Random, Time, Sql.".to_string(),
],
)
} else if message_lower.contains("record has no field") {
(
"Missing Field".to_string(),
vec!["Check the field name spelling or review the record definition.".to_string()],
)
} else if message_lower.contains("cannot access field") {
(
"Invalid Field Access".to_string(),
vec!["Field access is only valid on record types.".to_string()],
)
} else if message_lower.contains("effect") && (message_lower.contains("not available") || message_lower.contains("missing")) {
(
"Missing Effect".to_string(),
vec![
"Add the effect to your function's effect list.".to_string(),
"Example: fn myFn(): Int with {Console, Sql} = ...".to_string(),
],
)
} else {
("Type Error".to_string(), vec![])
}
@@ -448,6 +474,62 @@ fn is_structurally_decreasing(arg: &Expr, param_name: &str) -> bool {
}
}
/// Generate an auto-migration expression based on detected auto-migratable changes
/// Creates an expression like: { existingField1: old.existingField1, ..., newOptionalField: None }
fn generate_auto_migration_expr(
prev_def: &ast::TypeDef,
new_def: &ast::TypeDef,
auto_migrations: &[AutoMigration],
span: Span,
) -> Option<Expr> {
// Only handle record types for auto-migration
let (prev_fields, new_fields) = match (prev_def, new_def) {
(ast::TypeDef::Record(prev), ast::TypeDef::Record(new)) => (prev, new),
_ => return None,
};
// Build a record expression with all fields
let mut field_exprs: Vec<(ast::Ident, Expr)> = Vec::new();
// Map of new fields that need auto-migration defaults
let auto_migrate_fields: std::collections::HashSet<String> = auto_migrations
.iter()
.filter_map(|m| match m {
AutoMigration::AddFieldWithDefault { field_name, .. } => Some(field_name.clone()),
AutoMigration::WidenType { .. } => None,
})
.collect();
// For each field in the new definition
for new_field in new_fields {
let field_name = &new_field.name.name;
if auto_migrate_fields.contains(field_name) {
// New optional field - add with None default
field_exprs.push((
new_field.name.clone(),
Expr::Var(Ident::new("None", span)),
));
} else {
// Existing field - copy from old: old.fieldName
field_exprs.push((
new_field.name.clone(),
Expr::Field {
object: Box::new(Expr::Var(Ident::new("old", span))),
field: new_field.name.clone(),
span,
},
));
}
}
// Build the record expression
Some(Expr::Record {
fields: field_exprs,
span,
})
}
/// Check if a function terminates (structural recursion check)
fn check_termination(func: &FunctionDecl) -> Result<(), String> {
// Non-recursive functions always terminate
@@ -530,6 +612,12 @@ impl TypeChecker {
self.env.bindings.get(name)
}
/// Get auto-generated migrations from type checking
/// Returns: type_name -> from_version -> migration_body
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
&self.migrations
}
/// Type check a program
pub fn check_program(&mut self, program: &Program) -> Result<(), Vec<TypeError>> {
// First pass: collect all declarations
@@ -873,8 +961,28 @@ impl TypeChecker {
});
}
}
Ok(Compatibility::AutoMigrate(_)) | Ok(Compatibility::Compatible) => {
// No issues - compatible or auto-migratable
Ok(Compatibility::AutoMigrate(auto_migrations)) => {
// Generate automatic migration if one wasn't provided
if !self.migrations.get(&type_name).map(|m| m.contains_key(&prev_version)).unwrap_or(false) {
// Get the previous version's fields to build the migration
if let Some(prev_def) = self.schema_registry.get_version(&type_name, prev_version) {
if let Some(generated) = generate_auto_migration_expr(
&prev_def.definition,
&type_decl.definition,
&auto_migrations,
type_decl.name.span,
) {
// Register the auto-generated migration
self.migrations
.entry(type_name.clone())
.or_default()
.insert(prev_version, generated);
}
}
}
}
Ok(Compatibility::Compatible) => {
// No issues - fully compatible
}
Err(_) => {
// Previous version not registered yet - that's fine
@@ -974,7 +1082,7 @@ impl TypeChecker {
fn check_function(&mut self, func: &FunctionDecl) {
// Validate that all declared effects exist
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test"];
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test", "Sql"];
for effect in &func.effects {
let is_builtin = builtin_effects.contains(&effect.name.as_str());
let is_defined = self.env.lookup_effect(&effect.name).is_some();
@@ -1023,9 +1131,9 @@ impl TypeChecker {
self.current_effects = old_effects;
self.inferring_effects = old_inferring;
// Check that body type matches return type
// Check that body type matches return type (expand type aliases for record types)
let return_type = self.resolve_type(&func.return_type);
if let Err(e) = unify(&body_type, &return_type) {
if let Err(e) = unify_with_env(&body_type, &return_type, &self.env) {
self.errors.push(TypeError {
message: format!(
"Function '{}' body has type {}, but declared return type is {}: {}",
@@ -1656,10 +1764,36 @@ impl TypeChecker {
match unify(&func_type, &expected_fn) {
Ok(subst) => result_type.apply(&subst),
Err(e) => {
self.errors.push(TypeError {
message: format!("Type mismatch in function call: {}", e),
span,
});
// Provide more detailed error message based on the type of mismatch
let message = if e.contains("arity mismatch") || e.contains("different number") {
// Try to extract actual function arity
if let Type::Function { params, .. } = &func_type {
format!(
"Function expects {} argument(s), but {} were provided",
params.len(),
arg_types.len()
)
} else {
format!("Type mismatch in function call: {}", e)
}
} else if e.contains("Effect mismatch") {
format!("Type mismatch in function call: {}", e)
} else {
// Get function name if available for better error
let fn_name = if let Expr::Var(id) = func {
Some(id.name.clone())
} else {
None
};
if let Some(name) = fn_name {
format!("Type error in call to '{}': {}", name, e)
} else {
format!("Type mismatch in function call: {}", e)
}
};
self.errors.push(TypeError { message, span });
Type::Error
}
}
@@ -1729,7 +1863,7 @@ impl TypeChecker {
}
// Built-in effects are always available
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test"];
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test", "Sql"];
let is_builtin = builtin_effects.contains(&effect.name.as_str());
// Track this effect for inference
@@ -1814,10 +1948,18 @@ impl TypeChecker {
Type::Record(fields) => match fields.iter().find(|(n, _)| n == &field.name) {
Some((_, t)) => t.clone(),
None => {
self.errors.push(TypeError {
message: format!("Record has no field '{}'", field.name),
span,
});
// Find similar field names
let available_fields: Vec<&str> = fields.iter().map(|(n, _)| n.as_str()).collect();
let suggestions = find_similar_names(&field.name, available_fields.clone(), 2);
let mut message = format!("Record has no field '{}'", field.name);
if let Some(hint) = format_did_you_mean(&suggestions) {
message.push_str(&format!(". {}", hint));
} else if !available_fields.is_empty() {
message.push_str(&format!(". Available fields: {}", available_fields.join(", ")));
}
self.errors.push(TypeError { message, span });
Type::Error
}
},
@@ -1915,10 +2057,10 @@ impl TypeChecker {
) -> Type {
let value_type = self.infer_expr(value);
// Check declared type if present
// Check declared type if present (expand type aliases for record types)
if let Some(type_expr) = typ {
let declared = self.resolve_type(type_expr);
if let Err(e) = unify(&value_type, &declared) {
if let Err(e) = unify_with_env(&value_type, &declared, &self.env) {
self.errors.push(TypeError {
message: format!(
"Variable '{}' has type {}, but declared type is {}: {}",
@@ -2140,7 +2282,7 @@ impl TypeChecker {
.map(|(n, _)| (n.name.clone(), Type::var()))
.collect();
if let Err(e) = unify(expected, &Type::Record(field_types.clone())) {
if let Err(e) = unify_with_env(expected, &Type::Record(field_types.clone()), &self.env) {
self.errors.push(TypeError {
message: format!("Record pattern doesn't match type {}: {}", expected, e),
span: *span,
@@ -2234,7 +2376,7 @@ impl TypeChecker {
// Built-in effects are always available in run blocks (they have runtime implementations)
let builtin_effects: EffectSet =
EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer"].iter().map(|s| s.to_string()));
EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Sql"].iter().map(|s| s.to_string()));
// Extend current effects with handled ones and built-in effects
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);

View File

@@ -1173,6 +1173,73 @@ impl TypeEnv {
},
);
// Add Sql effect for database access
// Connection is represented as Int (connection ID)
let row_type = Type::Record(vec![]); // Dynamic record type
env.effects.insert(
"Sql".to_string(),
EffectDef {
name: "Sql".to_string(),
type_params: Vec::new(),
operations: vec![
EffectOpDef {
name: "open".to_string(),
params: vec![("path".to_string(), Type::String)],
return_type: Type::Int, // Connection ID
},
EffectOpDef {
name: "openMemory".to_string(),
params: vec![],
return_type: Type::Int, // Connection ID
},
EffectOpDef {
name: "close".to_string(),
params: vec![("conn".to_string(), Type::Int)],
return_type: Type::Unit,
},
EffectOpDef {
name: "execute".to_string(),
params: vec![
("conn".to_string(), Type::Int),
("sql".to_string(), Type::String),
],
return_type: Type::Int, // Rows affected
},
EffectOpDef {
name: "query".to_string(),
params: vec![
("conn".to_string(), Type::Int),
("sql".to_string(), Type::String),
],
return_type: Type::List(Box::new(Type::var())), // List of records
},
EffectOpDef {
name: "queryOne".to_string(),
params: vec![
("conn".to_string(), Type::Int),
("sql".to_string(), Type::String),
],
return_type: Type::Option(Box::new(Type::var())), // Optional record
},
EffectOpDef {
name: "beginTx".to_string(),
params: vec![("conn".to_string(), Type::Int)],
return_type: Type::Unit,
},
EffectOpDef {
name: "commit".to_string(),
params: vec![("conn".to_string(), Type::Int)],
return_type: Type::Unit,
},
EffectOpDef {
name: "rollback".to_string(),
params: vec![("conn".to_string(), Type::Int)],
return_type: Type::Unit,
},
],
},
);
// Add Some and Ok, Err constructors
// Some : fn(a) -> Option<a>
let a = Type::var();
@@ -1743,6 +1810,65 @@ impl TypeEnv {
TypeScheme::poly(type_vars, typ.clone())
}
/// Expand a Named type to its underlying structural type if it's an alias
/// This is needed for unifying record type aliases with record literals
pub fn expand_type_alias(&self, ty: &Type) -> Type {
match ty {
Type::Named(name) => {
if let Some(type_def) = self.types.get(name) {
match type_def {
TypeDef::Alias(inner) => self.expand_type_alias(inner),
// For enums and records, keep the Named type
_ => ty.clone(),
}
} else {
ty.clone()
}
}
Type::Function { params, return_type, effects, properties } => {
Type::Function {
params: params.iter().map(|p| self.expand_type_alias(p)).collect(),
return_type: Box::new(self.expand_type_alias(return_type)),
effects: effects.clone(),
properties: properties.clone(),
}
}
Type::App { constructor, args } => {
Type::App {
constructor: Box::new(self.expand_type_alias(constructor)),
args: args.iter().map(|a| self.expand_type_alias(a)).collect(),
}
}
Type::Tuple(elems) => {
Type::Tuple(elems.iter().map(|e| self.expand_type_alias(e)).collect())
}
Type::Record(fields) => {
Type::Record(fields.iter().map(|(n, t)| (n.clone(), self.expand_type_alias(t))).collect())
}
Type::List(inner) => {
Type::List(Box::new(self.expand_type_alias(inner)))
}
Type::Option(inner) => {
Type::Option(Box::new(self.expand_type_alias(inner)))
}
Type::Versioned { base, version } => {
Type::Versioned {
base: Box::new(self.expand_type_alias(base)),
version: version.clone(),
}
}
// Primitives and type variables stay as-is
_ => ty.clone(),
}
}
}
/// Unify types with type alias expansion
pub fn unify_with_env(t1: &Type, t2: &Type, env: &TypeEnv) -> Result<Substitution, String> {
let expanded1 = env.expand_type_alias(t1);
let expanded2 = env.expand_type_alias(t2);
unify(&expanded1, &expanded2)
}
/// Unification of types