From 4eebaebb27b9d28bedb3426b13b3d14ef92e77b3 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 09:59:52 -0500 Subject: [PATCH] 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 --- examples/versioning.lux | 35 +++++++++++++++++ src/interpreter.rs | 85 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 44 +++++++++++++++++++++ src/types.rs | 23 +++++++++++ 4 files changed, 187 insertions(+) create mode 100644 examples/versioning.lux diff --git a/examples/versioning.lux b/examples/versioning.lux new file mode 100644 index 0000000..d6c6781 --- /dev/null +++ b/examples/versioning.lux @@ -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 {} diff --git a/src/interpreter.rs b/src/interpreter.rs index 9e218e0..0f2722d 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -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")), + } + } } } diff --git a/src/main.rs b/src/main.rs index 876cf75..0b651f5 100644 --- a/src/main.rs +++ b/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 diff --git a/src/types.rs b/src/types.rs index 8ce6814..e028ec7 100644 --- a/src/types.rs +++ b/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 }