Refactor testing guidelines and add specializer module
Update the testing section to clarify when warnings should be eliminated and tests run. Add the new `specializer` module to the compiler's public API. Add the `type_registry` module to the `rtl` module's public API.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
pub mod core;
|
||||
pub mod datetime;
|
||||
pub mod type_registry;
|
||||
|
||||
use crate::ast::environment::Environment;
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
use std::collections::{HashMap, BTreeMap};
|
||||
use std::rc::Rc;
|
||||
use std::any::{TypeId};
|
||||
use crate::ast::types::{Value, StaticType, Keyword, Signature};
|
||||
|
||||
/// Represents a Rust type that can be exposed to the script environment.
|
||||
pub trait Scriptable: 'static + Sized {
|
||||
/// Returns the name of the type for debugging/AST.
|
||||
fn type_name() -> &'static str;
|
||||
|
||||
/// Returns the static type definition (methods, properties) for the AST.
|
||||
fn static_type() -> StaticType;
|
||||
|
||||
/// Wraps the instance into a Value (usually a Record of closures).
|
||||
fn wrap(self) -> Value;
|
||||
}
|
||||
|
||||
/// A registry for tracking registered types and their static definitions.
|
||||
pub struct TypeRegistry {
|
||||
known_types: HashMap<TypeId, StaticType>,
|
||||
}
|
||||
|
||||
impl Default for TypeRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
known_types: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a type T. Equivalent to `RegisterType<T>` in Delphi.
|
||||
pub fn register<T: Scriptable>(&mut self) {
|
||||
let id = TypeId::of::<T>();
|
||||
self.known_types.entry(id).or_insert_with(T::static_type);
|
||||
}
|
||||
|
||||
/// Resolves the static type for a Rust type.
|
||||
pub fn resolve_type<T: 'static>(&self) -> StaticType {
|
||||
self.known_types.get(&TypeId::of::<T>()).cloned().unwrap_or(StaticType::Any)
|
||||
}
|
||||
|
||||
/// Creates a factory function value that can be bound in the environment.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `factory_func` - A Rust closure that takes script arguments and returns a Result<T, String>.
|
||||
pub fn create_factory<T, F>(
|
||||
factory_func: F
|
||||
) -> Value
|
||||
where
|
||||
T: Scriptable,
|
||||
F: Fn(Vec<Value>) -> Result<T, String> + 'static
|
||||
{
|
||||
// The factory is a script function that calls the Rust factory, gets T, then wraps it.
|
||||
let closure = move |args: Vec<Value>| -> Value {
|
||||
match factory_func(args) {
|
||||
Ok(instance) => instance.wrap(),
|
||||
Err(msg) => {
|
||||
// In a real system, we'd propagate this error. For now, panic or return Void.
|
||||
// Delphi returns Void if nil, but raises exception on error.
|
||||
// Since Value doesn't have Error, we panic to stop execution.
|
||||
panic!("Runtime Error in Factory for {}: {}", T::type_name(), msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
Value::Function(Rc::new(closure))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to build the shadow record for an instance.
|
||||
/// This is used inside `Scriptable::wrap`.
|
||||
pub struct RecordBuilder {
|
||||
fields: HashMap<Keyword, Value>,
|
||||
}
|
||||
|
||||
impl Default for RecordBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RecordBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
fields: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method<F>(mut self, name: &str, func: F) -> Self
|
||||
where F: Fn(Vec<Value>) -> Value + 'static
|
||||
{
|
||||
let key = Keyword::intern(name);
|
||||
self.fields.insert(key, Value::Function(Rc::new(func)));
|
||||
self
|
||||
}
|
||||
|
||||
// Helper for methods that return Result (propagating panics for now)
|
||||
pub fn method_checked<F>(self, name: &str, func: F) -> Self
|
||||
where F: Fn(Vec<Value>) -> Result<Value, String> + 'static
|
||||
{
|
||||
let closure = move |args: Vec<Value>| {
|
||||
match func(args) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("Method call error: {}", e),
|
||||
}
|
||||
};
|
||||
self.method(name, closure)
|
||||
}
|
||||
|
||||
pub fn build(self) -> Value {
|
||||
Value::Record(Rc::new(self.fields))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to build the StaticType definition.
|
||||
pub struct TypeBuilder {
|
||||
fields: BTreeMap<Keyword, StaticType>,
|
||||
}
|
||||
|
||||
impl Default for TypeBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
fields: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method(mut self, name: &str, params: Vec<StaticType>, ret: StaticType) -> Self {
|
||||
let key = Keyword::intern(name);
|
||||
let sig = StaticType::Function(Box::new(Signature { params, ret }));
|
||||
self.fields.insert(key, sig);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StaticType {
|
||||
StaticType::Record(Rc::new(self.fields))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::types::{Value, StaticType};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl Scriptable for Person {
|
||||
fn type_name() -> &'static str { "Person" }
|
||||
|
||||
fn static_type() -> StaticType {
|
||||
TypeBuilder::new()
|
||||
.method("greet", vec![], StaticType::Text)
|
||||
.method("older", vec![], StaticType::Int)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn wrap(self) -> Value {
|
||||
let name = self.name.clone();
|
||||
let age = self.age;
|
||||
|
||||
RecordBuilder::new()
|
||||
.method("greet", move |_| Value::Text(format!("Hello {}", name).into()))
|
||||
.method("older", move |_| Value::Int((age + 1) as i64))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_and_wrap() {
|
||||
let mut registry = TypeRegistry::new();
|
||||
registry.register::<Person>();
|
||||
|
||||
let p = Person { name: "Alice".to_string(), age: 30 };
|
||||
let wrapped = p.wrap();
|
||||
|
||||
// Check static type
|
||||
let st = TypeRegistry::resolve_type::<Person>(®istry);
|
||||
if let StaticType::Record(fields) = st {
|
||||
assert!(fields.contains_key(&Keyword::intern("greet")));
|
||||
} else {
|
||||
panic!("Expected Record type");
|
||||
}
|
||||
|
||||
// Check runtime behavior
|
||||
if let Value::Record(fields) = wrapped {
|
||||
let greet_fn = fields.get(&Keyword::intern("greet")).unwrap();
|
||||
if let Value::Function(f) = greet_fn {
|
||||
let res = f(vec![]);
|
||||
if let Value::Text(s) = res {
|
||||
assert_eq!(&*s, "Hello Alice");
|
||||
} else {
|
||||
panic!("Expected Text result");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Function value for method");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Record value");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_factory() {
|
||||
let factory_val = TypeRegistry::create_factory(|args: Vec<Value>| {
|
||||
if args.len() != 2 {
|
||||
return Err("Expected 2 args".to_string());
|
||||
}
|
||||
let name = match &args[0] { Value::Text(t) => t.to_string(), _ => return Err("Name must be text".to_string()) };
|
||||
let age = match &args[1] { Value::Int(i) => *i as u32, _ => return Err("Age must be int".to_string()) };
|
||||
Ok(Person { name, age })
|
||||
});
|
||||
|
||||
if let Value::Function(f) = factory_val {
|
||||
let instance = f(vec![Value::Text("Bob".into()), Value::Int(40)]);
|
||||
if let Value::Record(fields) = instance {
|
||||
let greet_fn = fields.get(&Keyword::intern("greet")).unwrap();
|
||||
if let Value::Function(gf) = greet_fn {
|
||||
let res = gf(vec![]);
|
||||
if let Value::Text(s) = res {
|
||||
assert_eq!(&*s, "Hello Bob");
|
||||
} else { panic!("Wrong return type"); }
|
||||
}
|
||||
} else { panic!("Factory should return Record"); }
|
||||
} else { panic!("Factory is not a function"); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user