refactor: interpreter and type system improvements
- Parser and typechecker updates for new features - Schema evolution refinements - Type system enhancements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ pub enum BuiltinFn {
|
|||||||
ListLength,
|
ListLength,
|
||||||
ListGet,
|
ListGet,
|
||||||
ListRange,
|
ListRange,
|
||||||
|
ListForEach,
|
||||||
|
|
||||||
// String operations
|
// String operations
|
||||||
StringSplit,
|
StringSplit,
|
||||||
@@ -35,6 +36,8 @@ pub enum BuiltinFn {
|
|||||||
StringLength,
|
StringLength,
|
||||||
StringChars,
|
StringChars,
|
||||||
StringLines,
|
StringLines,
|
||||||
|
StringParseInt,
|
||||||
|
StringParseFloat,
|
||||||
|
|
||||||
// Option operations
|
// Option operations
|
||||||
OptionMap,
|
OptionMap,
|
||||||
@@ -842,6 +845,10 @@ impl Interpreter {
|
|||||||
("all".to_string(), Value::Builtin(BuiltinFn::ListAll)),
|
("all".to_string(), Value::Builtin(BuiltinFn::ListAll)),
|
||||||
("take".to_string(), Value::Builtin(BuiltinFn::ListTake)),
|
("take".to_string(), Value::Builtin(BuiltinFn::ListTake)),
|
||||||
("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)),
|
("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)),
|
||||||
|
(
|
||||||
|
"forEach".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::ListForEach),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
env.define("List", list_module);
|
env.define("List", list_module);
|
||||||
|
|
||||||
@@ -888,6 +895,14 @@ impl Interpreter {
|
|||||||
"fromChar".to_string(),
|
"fromChar".to_string(),
|
||||||
Value::Builtin(BuiltinFn::StringFromChar),
|
Value::Builtin(BuiltinFn::StringFromChar),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"parseInt".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringParseInt),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"parseFloat".to_string(),
|
||||||
|
Value::Builtin(BuiltinFn::StringParseFloat),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
env.define("String", string_module);
|
env.define("String", string_module);
|
||||||
|
|
||||||
@@ -1138,6 +1153,20 @@ impl Interpreter {
|
|||||||
self.global_env.define(&variant.name.name, constructor);
|
self.global_env.define(&variant.name.name, constructor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register migrations for versioned types
|
||||||
|
for migration in &type_decl.migrations {
|
||||||
|
let stored = StoredMigration {
|
||||||
|
body: migration.body.clone(),
|
||||||
|
env: self.global_env.clone(),
|
||||||
|
};
|
||||||
|
self.register_migration(
|
||||||
|
&type_decl.name.name,
|
||||||
|
migration.from_version.number,
|
||||||
|
stored,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Value::Unit)
|
Ok(Value::Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1615,11 +1644,10 @@ impl Interpreter {
|
|||||||
loop {
|
loop {
|
||||||
match result {
|
match result {
|
||||||
EvalResult::Value(v) => return Ok(v),
|
EvalResult::Value(v) => return Ok(v),
|
||||||
EvalResult::Effect(_) => {
|
EvalResult::Effect(req) => {
|
||||||
return Err(RuntimeError {
|
// Handle the effect and continue
|
||||||
message: "Effect in callback not supported".to_string(),
|
let handled = self.handle_effect(req)?;
|
||||||
span: Some(span),
|
return Ok(handled);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
EvalResult::TailCall { func, args, span } => {
|
EvalResult::TailCall { func, args, span } => {
|
||||||
result = self.eval_call(func, args, span)?;
|
result = self.eval_call(func, args, span)?;
|
||||||
@@ -1853,6 +1881,36 @@ impl Interpreter {
|
|||||||
Ok(EvalResult::Value(Value::List(lines)))
|
Ok(EvalResult::Value(Value::List(lines)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuiltinFn::StringParseInt => {
|
||||||
|
let s = Self::expect_arg_1::<String>(&args, "String.parseInt", span)?;
|
||||||
|
let trimmed = s.trim();
|
||||||
|
match trimmed.parse::<i64>() {
|
||||||
|
Ok(n) => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "Some".to_string(),
|
||||||
|
fields: vec![Value::Int(n)],
|
||||||
|
})),
|
||||||
|
Err(_) => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BuiltinFn::StringParseFloat => {
|
||||||
|
let s = Self::expect_arg_1::<String>(&args, "String.parseFloat", span)?;
|
||||||
|
let trimmed = s.trim();
|
||||||
|
match trimmed.parse::<f64>() {
|
||||||
|
Ok(f) => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "Some".to_string(),
|
||||||
|
fields: vec![Value::Float(f)],
|
||||||
|
})),
|
||||||
|
Err(_) => Ok(EvalResult::Value(Value::Constructor {
|
||||||
|
name: "None".to_string(),
|
||||||
|
fields: vec![],
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Option operations
|
// Option operations
|
||||||
BuiltinFn::OptionMap => {
|
BuiltinFn::OptionMap => {
|
||||||
let (opt, func) = Self::expect_args_2::<Value, Value>(&args, "Option.map", span)?;
|
let (opt, func) = Self::expect_args_2::<Value, Value>(&args, "Option.map", span)?;
|
||||||
@@ -2105,27 +2163,9 @@ impl Interpreter {
|
|||||||
Value::Int(n) => *n as u32,
|
Value::Int(n) => *n as u32,
|
||||||
_ => return Err(err("Schema.migrate: second argument must be an Int")),
|
_ => return Err(err("Schema.migrate: second argument must be an Int")),
|
||||||
};
|
};
|
||||||
match &args[0] {
|
// Use migrate_value which executes registered migrations
|
||||||
Value::Versioned { type_name, version, value } => {
|
let migrated = self.migrate_value(args[0].clone(), target)?;
|
||||||
if *version == target {
|
Ok(EvalResult::Value(migrated))
|
||||||
// Same version, return as-is
|
|
||||||
Ok(EvalResult::Value(args[0].clone()))
|
|
||||||
} else if *version < target {
|
|
||||||
// Upgrade - for now just update version (no migration logic)
|
|
||||||
Ok(EvalResult::Value(Value::Versioned {
|
|
||||||
type_name: type_name.clone(),
|
|
||||||
version: target,
|
|
||||||
value: value.clone(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Err(err(&format!(
|
|
||||||
"Cannot downgrade from version {} to {}",
|
|
||||||
version, target
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(err("Schema.migrate: first argument must be a Versioned value")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BuiltinFn::GetVersion => {
|
BuiltinFn::GetVersion => {
|
||||||
@@ -2327,6 +2367,18 @@ impl Interpreter {
|
|||||||
Ok(EvalResult::Value(Value::List(result)))
|
Ok(EvalResult::Value(Value::List(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BuiltinFn::ListForEach => {
|
||||||
|
// List.forEach(list, fn(item) => { effectful code })
|
||||||
|
// Unlike map, forEach doesn't collect results - it just runs effects
|
||||||
|
let (list, func) =
|
||||||
|
Self::expect_args_2::<Vec<Value>, Value>(&args, "List.forEach", span)?;
|
||||||
|
for item in list {
|
||||||
|
// Call the function for each item, ignoring the result
|
||||||
|
self.eval_call_to_value(func.clone(), vec![item], span)?;
|
||||||
|
}
|
||||||
|
Ok(EvalResult::Value(Value::Unit))
|
||||||
|
}
|
||||||
|
|
||||||
// Additional String operations
|
// Additional String operations
|
||||||
BuiltinFn::StringStartsWith => {
|
BuiltinFn::StringStartsWith => {
|
||||||
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
|
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
|
||||||
@@ -3967,4 +4019,69 @@ mod tests {
|
|||||||
_ => panic!("Expected Versioned value"),
|
_ => panic!("Expected Versioned value"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_migration_from_type_declaration() {
|
||||||
|
use crate::parser::Parser;
|
||||||
|
|
||||||
|
// Test that migrations defined in type declarations are registered and executed
|
||||||
|
let source = r#"
|
||||||
|
type User @v2 {
|
||||||
|
name: String,
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
from @v1 = { name: old.name, email: "default@example.com" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a v1 user using Schema.versioned
|
||||||
|
let v1_user = Schema.versioned("User", 1, { name: "Alice" })
|
||||||
|
|
||||||
|
// Migrate to v2 - should use the declared migration
|
||||||
|
let v2_user = Schema.migrate(v1_user, 2)
|
||||||
|
|
||||||
|
// Get the migrated value
|
||||||
|
let version = Schema.getVersion(v2_user)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let program = Parser::parse_source(source).expect("parse failed");
|
||||||
|
let mut interp = Interpreter::new();
|
||||||
|
let result = interp.run(&program);
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Interpreter failed: {:?}", result);
|
||||||
|
let result_value = result.unwrap();
|
||||||
|
|
||||||
|
// The last expression should be the version number
|
||||||
|
assert_eq!(format!("{}", result_value), "2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_migration_chain() {
|
||||||
|
use crate::parser::Parser;
|
||||||
|
|
||||||
|
// Test that migrations chain correctly: v1 -> v2 -> v3
|
||||||
|
let source = r#"
|
||||||
|
type Config @v3 {
|
||||||
|
host: String,
|
||||||
|
port: Int,
|
||||||
|
secure: Bool,
|
||||||
|
|
||||||
|
from @v2 = { host: old.host, port: old.port, secure: true },
|
||||||
|
from @v1 = { host: old.host, port: 8080 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create v1 config
|
||||||
|
let v1_config = Schema.versioned("Config", 1, { host: "localhost" })
|
||||||
|
|
||||||
|
// Migrate directly to v3 - should go v1 -> v2 -> v3
|
||||||
|
let v3_config = Schema.migrate(v1_config, 3)
|
||||||
|
let version = Schema.getVersion(v3_config)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let program = Parser::parse_source(source).expect("parse failed");
|
||||||
|
let mut interp = Interpreter::new();
|
||||||
|
let result = interp.run(&program);
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Interpreter failed: {:?}", result);
|
||||||
|
assert_eq!(format!("{}", result.unwrap()), "3");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,8 +479,11 @@ impl Parser {
|
|||||||
self.skip_newlines();
|
self.skip_newlines();
|
||||||
|
|
||||||
if self.check(TokenKind::Pipe) {
|
if self.check(TokenKind::Pipe) {
|
||||||
// Enum type - requires leading | for variants
|
// Enum type - with leading | for variants
|
||||||
(TypeDef::Enum(self.parse_variants()?), Vec::new())
|
(TypeDef::Enum(self.parse_variants()?), Vec::new())
|
||||||
|
} else if self.is_single_line_enum() {
|
||||||
|
// Single-line enum without leading pipe: type Status = Ok | Error
|
||||||
|
(TypeDef::Enum(self.parse_single_line_variants()?), Vec::new())
|
||||||
} else {
|
} else {
|
||||||
// Type alias - any type expression
|
// Type alias - any type expression
|
||||||
(TypeDef::Alias(self.parse_type()?), Vec::new())
|
(TypeDef::Alias(self.parse_type()?), Vec::new())
|
||||||
@@ -1253,6 +1256,85 @@ impl Parser {
|
|||||||
Ok(migrations)
|
Ok(migrations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if we're looking at a single-line enum (no leading pipe)
|
||||||
|
/// e.g., type Status = Ok | Error
|
||||||
|
fn is_single_line_enum(&self) -> bool {
|
||||||
|
// Look for pattern: Ident (LParen|LBrace)? Pipe
|
||||||
|
// We need to peek ahead without consuming tokens
|
||||||
|
if !matches!(&self.peek().kind, TokenKind::Ident(_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's a | somewhere on this logical line
|
||||||
|
// This is a simple heuristic - look at the next few tokens
|
||||||
|
let mut pos = self.pos;
|
||||||
|
let mut found_ident = false;
|
||||||
|
let mut depth = 0;
|
||||||
|
|
||||||
|
while pos < self.tokens.len() {
|
||||||
|
match &self.tokens[pos].kind {
|
||||||
|
TokenKind::Ident(_) if !found_ident => found_ident = true,
|
||||||
|
TokenKind::LParen | TokenKind::LBrace => depth += 1,
|
||||||
|
TokenKind::RParen | TokenKind::RBrace => {
|
||||||
|
if depth > 0 {
|
||||||
|
depth -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TokenKind::Pipe if depth == 0 => return true,
|
||||||
|
TokenKind::Newline | TokenKind::Eof => return false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse single-line variants without leading pipe
|
||||||
|
fn parse_single_line_variants(&mut self) -> Result<Vec<Variant>, ParseError> {
|
||||||
|
let mut variants = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let start = self.current_span();
|
||||||
|
let name = self.parse_ident()?;
|
||||||
|
|
||||||
|
let fields = if self.check(TokenKind::LParen) {
|
||||||
|
// Tuple variant
|
||||||
|
self.advance();
|
||||||
|
let mut types = Vec::new();
|
||||||
|
while !self.check(TokenKind::RParen) {
|
||||||
|
types.push(self.parse_type()?);
|
||||||
|
if !self.check(TokenKind::RParen) {
|
||||||
|
self.expect(TokenKind::Comma)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.expect(TokenKind::RParen)?;
|
||||||
|
VariantFields::Tuple(types)
|
||||||
|
} else if self.check(TokenKind::LBrace) {
|
||||||
|
// Record variant
|
||||||
|
self.advance();
|
||||||
|
self.skip_newlines();
|
||||||
|
let fields = self.parse_record_fields()?;
|
||||||
|
self.expect(TokenKind::RBrace)?;
|
||||||
|
VariantFields::Record(fields)
|
||||||
|
} else {
|
||||||
|
VariantFields::Unit
|
||||||
|
};
|
||||||
|
|
||||||
|
let span = start.merge(self.previous_span());
|
||||||
|
variants.push(Variant { name, fields, span });
|
||||||
|
|
||||||
|
// Check for more variants
|
||||||
|
if self.check(TokenKind::Pipe) {
|
||||||
|
self.advance();
|
||||||
|
self.skip_newlines();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(variants)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_variants(&mut self) -> Result<Vec<Variant>, ParseError> {
|
fn parse_variants(&mut self) -> Result<Vec<Variant>, ParseError> {
|
||||||
let mut variants = Vec::new();
|
let mut variants = Vec::new();
|
||||||
|
|
||||||
@@ -1660,9 +1742,19 @@ impl Parser {
|
|||||||
let then_branch = Box::new(self.parse_expr()?);
|
let then_branch = Box::new(self.parse_expr()?);
|
||||||
|
|
||||||
self.skip_newlines();
|
self.skip_newlines();
|
||||||
|
|
||||||
|
// Else is optional - if missing, synthesize a Unit value
|
||||||
|
let else_branch = if self.check(TokenKind::Else) {
|
||||||
self.expect(TokenKind::Else)?;
|
self.expect(TokenKind::Else)?;
|
||||||
self.skip_newlines();
|
self.skip_newlines();
|
||||||
let else_branch = Box::new(self.parse_expr()?);
|
Box::new(self.parse_expr()?)
|
||||||
|
} else {
|
||||||
|
// No else clause - use Unit as the else branch
|
||||||
|
Box::new(Expr::Literal(Literal {
|
||||||
|
kind: LiteralKind::Unit,
|
||||||
|
span: then_branch.span(),
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
let span = start.merge(else_branch.span());
|
let span = start.merge(else_branch.span());
|
||||||
Ok(Expr::If {
|
Ok(Expr::If {
|
||||||
|
|||||||
@@ -199,6 +199,67 @@ fn compare_type_defs(from: &TypeDef, to: &TypeDef) -> Result<Compatibility, Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compare two TypeExpr for semantic equality (ignoring spans)
|
||||||
|
fn types_equal(a: &crate::ast::TypeExpr, b: &crate::ast::TypeExpr) -> bool {
|
||||||
|
use crate::ast::TypeExpr;
|
||||||
|
match (a, b) {
|
||||||
|
(TypeExpr::Named(a_ident), TypeExpr::Named(b_ident)) => {
|
||||||
|
a_ident.name == b_ident.name
|
||||||
|
}
|
||||||
|
(TypeExpr::App(a_base, a_args), TypeExpr::App(b_base, b_args)) => {
|
||||||
|
types_equal(a_base, b_base)
|
||||||
|
&& a_args.len() == b_args.len()
|
||||||
|
&& a_args.iter().zip(b_args.iter()).all(|(a, b)| types_equal(a, b))
|
||||||
|
}
|
||||||
|
(
|
||||||
|
TypeExpr::Function {
|
||||||
|
params: a_params,
|
||||||
|
return_type: a_ret,
|
||||||
|
effects: a_effects,
|
||||||
|
},
|
||||||
|
TypeExpr::Function {
|
||||||
|
params: b_params,
|
||||||
|
return_type: b_ret,
|
||||||
|
effects: b_effects,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
a_params.len() == b_params.len()
|
||||||
|
&& a_params.iter().zip(b_params.iter()).all(|(a, b)| types_equal(a, b))
|
||||||
|
&& types_equal(a_ret, b_ret)
|
||||||
|
&& a_effects.len() == b_effects.len()
|
||||||
|
&& a_effects.iter().zip(b_effects.iter()).all(|(a, b)| a.name == b.name)
|
||||||
|
}
|
||||||
|
(TypeExpr::Tuple(a_elems), TypeExpr::Tuple(b_elems)) => {
|
||||||
|
a_elems.len() == b_elems.len()
|
||||||
|
&& a_elems.iter().zip(b_elems.iter()).all(|(a, b)| types_equal(a, b))
|
||||||
|
}
|
||||||
|
(TypeExpr::Record(a_fields), TypeExpr::Record(b_fields)) => {
|
||||||
|
if a_fields.len() != b_fields.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let a_map: HashMap<&str, &crate::ast::RecordField> =
|
||||||
|
a_fields.iter().map(|f| (f.name.name.as_str(), f)).collect();
|
||||||
|
for b_field in b_fields {
|
||||||
|
match a_map.get(b_field.name.name.as_str()) {
|
||||||
|
Some(a_field) => {
|
||||||
|
if !types_equal(&a_field.typ, &b_field.typ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
(TypeExpr::Unit, TypeExpr::Unit) => true,
|
||||||
|
(TypeExpr::Versioned { base: a_base, .. }, TypeExpr::Versioned { base: b_base, .. }) => {
|
||||||
|
// For versioned types, just compare the base type names
|
||||||
|
types_equal(a_base, b_base)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Compare record fields for compatibility
|
/// Compare record fields for compatibility
|
||||||
fn compare_record_fields(
|
fn compare_record_fields(
|
||||||
from: &[RecordField],
|
from: &[RecordField],
|
||||||
@@ -243,7 +304,7 @@ fn compare_record_fields(
|
|||||||
// Check for type changes in existing fields
|
// Check for type changes in existing fields
|
||||||
for (name, from_field) in &from_map {
|
for (name, from_field) in &from_map {
|
||||||
if let Some(to_field) = to_map.get(name) {
|
if let Some(to_field) = to_map.get(name) {
|
||||||
if from_field.typ != to_field.typ {
|
if !types_equal(&from_field.typ, &to_field.typ) {
|
||||||
// Types differ - check if it's a compatible widening
|
// Types differ - check if it's a compatible widening
|
||||||
// For now, treat all type changes as breaking
|
// For now, treat all type changes as breaking
|
||||||
breaking_changes.push(BreakingChange::FieldTypeChanged {
|
breaking_changes.push(BreakingChange::FieldTypeChanged {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use crate::ast::{
|
|||||||
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
|
use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Severity};
|
||||||
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
||||||
use crate::modules::ModuleLoader;
|
use crate::modules::ModuleLoader;
|
||||||
|
use crate::schema::{SchemaRegistry, Compatibility, BreakingChange};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, Property, PropertySet,
|
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, Property, PropertySet,
|
||||||
TraitBoundDef, TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef,
|
TraitBoundDef, TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef,
|
||||||
@@ -480,6 +481,7 @@ fn check_termination(func: &FunctionDecl) -> Result<(), String> {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ParamPropertyConstraint {
|
pub struct ParamPropertyConstraint {
|
||||||
pub param_name: String,
|
pub param_name: String,
|
||||||
|
pub param_index: usize,
|
||||||
pub required_properties: PropertySet,
|
pub required_properties: PropertySet,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +504,8 @@ pub struct TypeChecker {
|
|||||||
latest_versions: HashMap<String, u32>,
|
latest_versions: HashMap<String, u32>,
|
||||||
/// Migrations: type_name -> source_version -> migration_body
|
/// Migrations: type_name -> source_version -> migration_body
|
||||||
migrations: HashMap<String, HashMap<u32, Expr>>,
|
migrations: HashMap<String, HashMap<u32, Expr>>,
|
||||||
|
/// Schema registry for compatibility checking
|
||||||
|
schema_registry: SchemaRegistry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeChecker {
|
impl TypeChecker {
|
||||||
@@ -517,6 +521,7 @@ impl TypeChecker {
|
|||||||
versioned_types: HashMap::new(),
|
versioned_types: HashMap::new(),
|
||||||
latest_versions: HashMap::new(),
|
latest_versions: HashMap::new(),
|
||||||
migrations: HashMap::new(),
|
migrations: HashMap::new(),
|
||||||
|
schema_registry: SchemaRegistry::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,6 +831,57 @@ impl TypeChecker {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.insert(migration.from_version.number, migration.body.clone());
|
.insert(migration.from_version.number, migration.body.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register in schema registry and check compatibility
|
||||||
|
self.schema_registry.register(&type_name, type_decl);
|
||||||
|
|
||||||
|
// Check compatibility with previous version if this isn't v1
|
||||||
|
if version_num > 1 {
|
||||||
|
let prev_version = version_num - 1;
|
||||||
|
|
||||||
|
// Check if both versions are registered
|
||||||
|
if self.schema_registry.get_version(&type_name, prev_version).is_some() {
|
||||||
|
match self.schema_registry.check_compatibility(&type_name, prev_version, version_num) {
|
||||||
|
Ok(Compatibility::Breaking(changes)) => {
|
||||||
|
// Check if a migration exists for the breaking changes
|
||||||
|
let has_migration = self.schema_registry.has_migration(&type_name, prev_version, version_num);
|
||||||
|
|
||||||
|
if !has_migration {
|
||||||
|
// No migration for breaking changes - this is a warning
|
||||||
|
let change_descriptions: Vec<String> = changes.iter().map(|c| {
|
||||||
|
match c {
|
||||||
|
BreakingChange::FieldRemoved { field_name } =>
|
||||||
|
format!("field '{}' removed", field_name),
|
||||||
|
BreakingChange::FieldRenamed { old_name, new_name } =>
|
||||||
|
format!("field '{}' renamed to '{}'", old_name, new_name),
|
||||||
|
BreakingChange::FieldTypeChanged { field_name, old_type, new_type } =>
|
||||||
|
format!("field '{}' type changed from {} to {}", field_name, old_type, new_type),
|
||||||
|
BreakingChange::RequiredFieldAdded { field_name } =>
|
||||||
|
format!("required field '{}' added", field_name),
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
self.errors.push(TypeError {
|
||||||
|
message: format!(
|
||||||
|
"Breaking changes in {} @v{} without migration from @v{}: {}. \
|
||||||
|
Add 'from @v{} = {{ ... }}' to provide a migration.",
|
||||||
|
type_name, version_num, prev_version,
|
||||||
|
change_descriptions.join(", "),
|
||||||
|
prev_version
|
||||||
|
),
|
||||||
|
span: type_decl.name.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Compatibility::AutoMigrate(_)) | Ok(Compatibility::Compatible) => {
|
||||||
|
// No issues - compatible or auto-migratable
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Previous version not registered yet - that's fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register ADT constructors as values with polymorphic types
|
// Register ADT constructors as values with polymorphic types
|
||||||
@@ -1128,7 +1184,13 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(param) = param_with_type {
|
if let Some((param_index, param)) = func.params.iter().enumerate().find(|(_, p)| {
|
||||||
|
if let TypeExpr::Named(name) = &p.typ {
|
||||||
|
name.name == type_param.name
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
// Record the constraint for checking at call sites
|
// Record the constraint for checking at call sites
|
||||||
let constraints = self
|
let constraints = self
|
||||||
.property_constraints
|
.property_constraints
|
||||||
@@ -1143,6 +1205,7 @@ impl TypeChecker {
|
|||||||
props.insert(Property::from(*property));
|
props.insert(Property::from(*property));
|
||||||
constraints.push(ParamPropertyConstraint {
|
constraints.push(ParamPropertyConstraint {
|
||||||
param_name: param.name.name.clone(),
|
param_name: param.name.name.clone(),
|
||||||
|
param_index,
|
||||||
required_properties: props,
|
required_properties: props,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1557,29 +1620,18 @@ impl TypeChecker {
|
|||||||
// Check property constraints from where clauses
|
// Check property constraints from where clauses
|
||||||
if let Expr::Var(func_id) = func {
|
if let Expr::Var(func_id) = func {
|
||||||
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
|
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
|
||||||
// Get parameter names from the function declaration (if available in env)
|
|
||||||
// We'll match by position since we have the constraints by param name
|
|
||||||
if let Some(scheme) = self.env.lookup(&func_id.name) {
|
|
||||||
let func_typ = scheme.instantiate();
|
|
||||||
if let Type::Function { params: param_types, .. } = &func_typ {
|
|
||||||
for constraint in &constraints {
|
for constraint in &constraints {
|
||||||
// Find which argument position corresponds to this param
|
// Check if the argument at the constrained position satisfies the constraint
|
||||||
// For now, match by position based on stored param names
|
if constraint.param_index < args.len() {
|
||||||
for (i, arg) in args.iter().enumerate() {
|
let arg = &args[constraint.param_index];
|
||||||
// Get the properties of the argument
|
|
||||||
let arg_props = self.get_expr_properties(arg);
|
let arg_props = self.get_expr_properties(arg);
|
||||||
|
|
||||||
// Check if this argument corresponds to a constrained param
|
|
||||||
// We check all constraints and verify the arg satisfies them
|
|
||||||
if !arg_props.satisfies(&constraint.required_properties) {
|
if !arg_props.satisfies(&constraint.required_properties) {
|
||||||
// Only report if this argument could be the constrained one
|
|
||||||
// (simple heuristic: function type argument)
|
|
||||||
if i < param_types.len() {
|
|
||||||
if let Type::Function { .. } = ¶m_types[i] {
|
|
||||||
self.errors.push(TypeError {
|
self.errors.push(TypeError {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Argument to '{}' does not satisfy property constraint: \
|
"Argument '{}' to '{}' does not satisfy property constraint: \
|
||||||
expected {:?}, but argument has {:?}",
|
expected {:?}, but argument has {:?}",
|
||||||
|
constraint.param_name,
|
||||||
func_id.name,
|
func_id.name,
|
||||||
constraint.required_properties,
|
constraint.required_properties,
|
||||||
arg_props
|
arg_props
|
||||||
@@ -1591,10 +1643,6 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result_type = Type::var();
|
let result_type = Type::var();
|
||||||
// Include current effects in the expected function type
|
// Include current effects in the expected function type
|
||||||
|
|||||||
18
src/types.rs
18
src/types.rs
@@ -1343,6 +1343,16 @@ impl TypeEnv {
|
|||||||
Type::List(Box::new(Type::var())),
|
Type::List(Box::new(Type::var())),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"forEach".to_string(),
|
||||||
|
Type::function(
|
||||||
|
vec![
|
||||||
|
Type::List(Box::new(Type::var())),
|
||||||
|
Type::function(vec![Type::var()], Type::Unit),
|
||||||
|
],
|
||||||
|
Type::Unit,
|
||||||
|
),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
env.bind("List", TypeScheme::mono(list_module_type));
|
env.bind("List", TypeScheme::mono(list_module_type));
|
||||||
|
|
||||||
@@ -1410,6 +1420,14 @@ impl TypeEnv {
|
|||||||
"fromChar".to_string(),
|
"fromChar".to_string(),
|
||||||
Type::function(vec![Type::Char], Type::String),
|
Type::function(vec![Type::Char], Type::String),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"parseInt".to_string(),
|
||||||
|
Type::function(vec![Type::String], Type::Option(Box::new(Type::Int))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"parseFloat".to_string(),
|
||||||
|
Type::function(vec![Type::String], Type::Option(Box::new(Type::Float))),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
env.bind("String", TypeScheme::mono(string_module_type));
|
env.bind("String", TypeScheme::mono(string_module_type));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user