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,
|
||||
ListGet,
|
||||
ListRange,
|
||||
ListForEach,
|
||||
|
||||
// String operations
|
||||
StringSplit,
|
||||
@@ -35,6 +36,8 @@ pub enum BuiltinFn {
|
||||
StringLength,
|
||||
StringChars,
|
||||
StringLines,
|
||||
StringParseInt,
|
||||
StringParseFloat,
|
||||
|
||||
// Option operations
|
||||
OptionMap,
|
||||
@@ -842,6 +845,10 @@ impl Interpreter {
|
||||
("all".to_string(), Value::Builtin(BuiltinFn::ListAll)),
|
||||
("take".to_string(), Value::Builtin(BuiltinFn::ListTake)),
|
||||
("drop".to_string(), Value::Builtin(BuiltinFn::ListDrop)),
|
||||
(
|
||||
"forEach".to_string(),
|
||||
Value::Builtin(BuiltinFn::ListForEach),
|
||||
),
|
||||
]));
|
||||
env.define("List", list_module);
|
||||
|
||||
@@ -888,6 +895,14 @@ impl Interpreter {
|
||||
"fromChar".to_string(),
|
||||
Value::Builtin(BuiltinFn::StringFromChar),
|
||||
),
|
||||
(
|
||||
"parseInt".to_string(),
|
||||
Value::Builtin(BuiltinFn::StringParseInt),
|
||||
),
|
||||
(
|
||||
"parseFloat".to_string(),
|
||||
Value::Builtin(BuiltinFn::StringParseFloat),
|
||||
),
|
||||
]));
|
||||
env.define("String", string_module);
|
||||
|
||||
@@ -1138,6 +1153,20 @@ impl Interpreter {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1615,11 +1644,10 @@ impl Interpreter {
|
||||
loop {
|
||||
match result {
|
||||
EvalResult::Value(v) => return Ok(v),
|
||||
EvalResult::Effect(_) => {
|
||||
return Err(RuntimeError {
|
||||
message: "Effect in callback not supported".to_string(),
|
||||
span: Some(span),
|
||||
});
|
||||
EvalResult::Effect(req) => {
|
||||
// Handle the effect and continue
|
||||
let handled = self.handle_effect(req)?;
|
||||
return Ok(handled);
|
||||
}
|
||||
EvalResult::TailCall { func, args, span } => {
|
||||
result = self.eval_call(func, args, span)?;
|
||||
@@ -1853,6 +1881,36 @@ impl Interpreter {
|
||||
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
|
||||
BuiltinFn::OptionMap => {
|
||||
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,
|
||||
_ => return Err(err("Schema.migrate: second argument must be an Int")),
|
||||
};
|
||||
match &args[0] {
|
||||
Value::Versioned { type_name, version, value } => {
|
||||
if *version == target {
|
||||
// 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")),
|
||||
}
|
||||
// Use migrate_value which executes registered migrations
|
||||
let migrated = self.migrate_value(args[0].clone(), target)?;
|
||||
Ok(EvalResult::Value(migrated))
|
||||
}
|
||||
|
||||
BuiltinFn::GetVersion => {
|
||||
@@ -2327,6 +2367,18 @@ impl Interpreter {
|
||||
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
|
||||
BuiltinFn::StringStartsWith => {
|
||||
let (s, prefix) = Self::expect_args_2::<String, String>(&args, "String.startsWith", span)?;
|
||||
@@ -3967,4 +4019,69 @@ mod tests {
|
||||
_ => 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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user