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:
@@ -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);");
|
||||
|
||||
@@ -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);
|
||||
|
||||
126
src/types.rs
126
src/types.rs
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user