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:
2026-02-13 09:59:52 -05:00
parent 9511957076
commit 4eebaebb27
4 changed files with 187 additions and 0 deletions

35
examples/versioning.lux Normal file
View 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 {}

View File

@@ -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")),
}
}
}
}

View File

@@ -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

View File

@@ -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
}