feat: implement Schema module for versioned types
Add Schema module with functions for creating and migrating versioned values. This provides the runtime foundation for schema evolution. Schema module functions: - Schema.versioned(typeName, version, value) - create versioned value - Schema.migrate(value, targetVersion) - migrate to new version - Schema.getVersion(value) - get version number Changes: - Add Versioned, Migrate, GetVersion builtins to interpreter - Add Schema module to global environment - Add Schema module type to type environment - Add 4 tests for schema operations - Add examples/versioning.lux demonstrating usage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,11 @@ pub enum BuiltinFn {
|
||||
Print,
|
||||
ToString,
|
||||
TypeOf,
|
||||
|
||||
// Schema Evolution
|
||||
Versioned, // Create versioned value: versioned("TypeName", 1, value)
|
||||
Migrate, // Migrate to version: migrate(versionedValue, targetVersion)
|
||||
GetVersion, // Get version number: getVersion(versionedValue)
|
||||
}
|
||||
|
||||
/// Runtime value
|
||||
@@ -770,6 +775,20 @@ impl Interpreter {
|
||||
env.define("print", Value::Builtin(BuiltinFn::Print));
|
||||
env.define("toString", Value::Builtin(BuiltinFn::ToString));
|
||||
env.define("typeOf", Value::Builtin(BuiltinFn::TypeOf));
|
||||
|
||||
// Schema Evolution module
|
||||
let schema_module = Value::Record(HashMap::from([
|
||||
(
|
||||
"versioned".to_string(),
|
||||
Value::Builtin(BuiltinFn::Versioned),
|
||||
),
|
||||
("migrate".to_string(), Value::Builtin(BuiltinFn::Migrate)),
|
||||
(
|
||||
"getVersion".to_string(),
|
||||
Value::Builtin(BuiltinFn::GetVersion),
|
||||
),
|
||||
]));
|
||||
env.define("Schema", schema_module);
|
||||
}
|
||||
|
||||
/// Execute a program
|
||||
@@ -1860,6 +1879,72 @@ impl Interpreter {
|
||||
args[0].type_name().to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
// Schema Evolution
|
||||
BuiltinFn::Versioned => {
|
||||
// versioned(typeName: String, version: Int, value: Any) -> Versioned
|
||||
if args.len() != 3 {
|
||||
return Err(err("Schema.versioned requires 3 arguments: typeName, version, value"));
|
||||
}
|
||||
let type_name = match &args[0] {
|
||||
Value::String(s) => s.clone(),
|
||||
_ => return Err(err("Schema.versioned: first argument must be a String")),
|
||||
};
|
||||
let version = match &args[1] {
|
||||
Value::Int(n) => *n as u32,
|
||||
_ => return Err(err("Schema.versioned: second argument must be an Int")),
|
||||
};
|
||||
Ok(EvalResult::Value(Value::Versioned {
|
||||
type_name,
|
||||
version,
|
||||
value: Box::new(args[2].clone()),
|
||||
}))
|
||||
}
|
||||
|
||||
BuiltinFn::Migrate => {
|
||||
// migrate(value: Versioned, targetVersion: Int) -> Versioned
|
||||
if args.len() != 2 {
|
||||
return Err(err("Schema.migrate requires 2 arguments: value, targetVersion"));
|
||||
}
|
||||
let target = match &args[1] {
|
||||
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")),
|
||||
}
|
||||
}
|
||||
|
||||
BuiltinFn::GetVersion => {
|
||||
// getVersion(value: Versioned) -> Int
|
||||
if args.len() != 1 {
|
||||
return Err(err("Schema.getVersion requires 1 argument"));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Versioned { version, .. } => {
|
||||
Ok(EvalResult::Value(Value::Int(*version as i64)))
|
||||
}
|
||||
_ => Err(err("Schema.getVersion: argument must be a Versioned value")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
44
src/main.rs
44
src/main.rs
@@ -1333,6 +1333,50 @@ c")"#;
|
||||
assert!(err_msg.contains("outside") || err_msg.contains("Resume"),
|
||||
"Error should mention resume outside handler: {}", err_msg);
|
||||
}
|
||||
|
||||
// Schema Evolution tests
|
||||
#[test]
|
||||
fn test_schema_versioned() {
|
||||
let source = r#"
|
||||
let user = Schema.versioned("User", 1, { name: "Alice", age: 30 })
|
||||
let version = Schema.getVersion(user)
|
||||
"#;
|
||||
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
||||
assert_eq!(result, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_migrate_same_version() {
|
||||
let source = r#"
|
||||
let user = Schema.versioned("User", 2, { name: "Bob" })
|
||||
let migrated = Schema.migrate(user, 2)
|
||||
let version = Schema.getVersion(migrated)
|
||||
"#;
|
||||
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
||||
assert_eq!(result, "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_migrate_upgrade() {
|
||||
let source = r#"
|
||||
let user = Schema.versioned("User", 1, { name: "Charlie" })
|
||||
let migrated = Schema.migrate(user, 3)
|
||||
let version = Schema.getVersion(migrated)
|
||||
"#;
|
||||
let (result, _) = run_with_effects(source, Value::Unit, Value::Unit).unwrap();
|
||||
assert_eq!(result, "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_migrate_downgrade_fails() {
|
||||
let source = r#"
|
||||
let user = Schema.versioned("User", 3, { name: "Dave" })
|
||||
let migrated = Schema.migrate(user, 1)
|
||||
"#;
|
||||
let result = run_with_effects(source, Value::Unit, Value::Unit);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("downgrade"));
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic rendering tests
|
||||
|
||||
23
src/types.rs
23
src/types.rs
@@ -1115,6 +1115,29 @@ impl TypeEnv {
|
||||
TypeScheme::mono(Type::function(vec![Type::var()], Type::String)),
|
||||
);
|
||||
|
||||
// Schema module for versioned types
|
||||
let schema_module_type = Type::Record(vec![
|
||||
(
|
||||
"versioned".to_string(),
|
||||
Type::function(
|
||||
vec![Type::String, Type::Int, Type::var()],
|
||||
Type::var(), // Returns Versioned (treated as Any for now)
|
||||
),
|
||||
),
|
||||
(
|
||||
"migrate".to_string(),
|
||||
Type::function(
|
||||
vec![Type::var(), Type::Int],
|
||||
Type::var(), // Returns Versioned
|
||||
),
|
||||
),
|
||||
(
|
||||
"getVersion".to_string(),
|
||||
Type::function(vec![Type::var()], Type::Int),
|
||||
),
|
||||
]);
|
||||
env.bind("Schema", TypeScheme::mono(schema_module_type));
|
||||
|
||||
env
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user