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:
35
examples/versioning.lux
Normal file
35
examples/versioning.lux
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Demonstrating Schema Evolution in Lux
|
||||||
|
//
|
||||||
|
// Lux provides versioned types to help manage data evolution over time.
|
||||||
|
// The Schema module provides functions for creating and migrating versioned values.
|
||||||
|
//
|
||||||
|
// Expected output:
|
||||||
|
// Created user v1: Alice (age unknown)
|
||||||
|
// User version: 1
|
||||||
|
// Migrated to v2: Alice (age unknown)
|
||||||
|
// User version after migration: 2
|
||||||
|
|
||||||
|
// Create a versioned User value at v1
|
||||||
|
fn createUserV1(name: String): Unit with {Console} = {
|
||||||
|
let user = Schema.versioned("User", 1, { name: name })
|
||||||
|
let version = Schema.getVersion(user)
|
||||||
|
Console.print("Created user v1: " + name + " (age unknown)")
|
||||||
|
Console.print("User version: " + toString(version))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate a user to v2
|
||||||
|
fn migrateUserToV2(name: String): Unit with {Console} = {
|
||||||
|
let userV1 = Schema.versioned("User", 1, { name: name })
|
||||||
|
let userV2 = Schema.migrate(userV1, 2)
|
||||||
|
let newVersion = Schema.getVersion(userV2)
|
||||||
|
Console.print("Migrated to v2: " + name + " (age unknown)")
|
||||||
|
Console.print("User version after migration: " + toString(newVersion))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
fn main(): Unit with {Console} = {
|
||||||
|
createUserV1("Alice")
|
||||||
|
migrateUserToV2("Alice")
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = run main() with {}
|
||||||
@@ -53,6 +53,11 @@ pub enum BuiltinFn {
|
|||||||
Print,
|
Print,
|
||||||
ToString,
|
ToString,
|
||||||
TypeOf,
|
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
|
/// Runtime value
|
||||||
@@ -770,6 +775,20 @@ impl Interpreter {
|
|||||||
env.define("print", Value::Builtin(BuiltinFn::Print));
|
env.define("print", Value::Builtin(BuiltinFn::Print));
|
||||||
env.define("toString", Value::Builtin(BuiltinFn::ToString));
|
env.define("toString", Value::Builtin(BuiltinFn::ToString));
|
||||||
env.define("typeOf", Value::Builtin(BuiltinFn::TypeOf));
|
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
|
/// Execute a program
|
||||||
@@ -1860,6 +1879,72 @@ impl Interpreter {
|
|||||||
args[0].type_name().to_string(),
|
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"),
|
assert!(err_msg.contains("outside") || err_msg.contains("Resume"),
|
||||||
"Error should mention resume outside handler: {}", err_msg);
|
"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
|
// 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)),
|
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
|
env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user