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

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