Metaprogramming
Advanced runtime code generation and introspection capabilities for building dynamic, expressive applications.
Available: respond_to?, send, method_missing, instance_variables, instance_variable_get, instance_variable_set, methods, instance_eval, class_eval, define_method, alias_method, inherited. See tests/language/metaprogramming_spec.sl for examples.
Dynamic Method Dispatch: send()
Call a method by its name as a string. This enables dynamic method invocation at runtime.
Soli
class Person
def greet
"Hello!"
end
def greet_with(name)
"Hello, " + name + "!"
end
end
let person = Person.new
person.send("greet") # => "Hello!"
person.send("greet_with", "World") # => "Hello, World!"
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs within instance_member_access:
// Implementation flow:
// 1. Check native methods via find_native_method
// 2. Check user-defined methods via find_method
// 3. If neither found, check for method_missing and call it
// 4. If method_missing exists, execute it with method name and args
// 5. Return error "undefined method" if nothing found
Ghost Methods: method_missing()
Called automatically when code tries to invoke a method that doesn't exist. This enables "ghost methods" - methods that appear to exist but are dynamically handled.
Soli
class Person
def method_missing(method_name)
if (method_name.starts_with("find_by_"))
let field = method_name.sub("find_by_", "")
return "Finding person by " + field
end
return "Method '" + method_name + "' was called"
end
end
let person = Person.new
person.find_by_name("Alice") # => "Finding person by name"
person.find_by_email("[email protected]") # => "Finding person by email"
person.any_undefined_method # => "Method 'any_undefined_method' was called"
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs at the end of instance_member_access:
// When a method is not found:
// 1. Check if class has method_missing defined
// 2. If yes, return a NativeFunction that wraps method_missing
// 3. When invoked, builds args [method_name, ...original_args]
// 4. Executes method_missing with proper 'this' binding
// 5. Returns error if method_missing also not found
Introspection: respond_to?()
Check if an object can respond to a method call. Essential for duck typing and safe dynamic dispatch.
Soli
class Person
def greet
"Hello"
end
end
let person = Person.new
person.respond_to?("greet") # => true
person.respond_to?("find_by_name") # => false
person.respond_to?("send") # => true
person.respond_to?("inspect") # => true (universal method)
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs as a universal instance method:
// Checks class methods, native methods, AND universal methods
// Universal methods (inspect, class, nil?, etc.) are handled inline
// and need explicit inclusion in respond_to? checks
let is_universal = matches!(method_name.as_str(),
"inspect" | "class" | "nil?" | "blank?" | "present?"
| "respond_to?" | "send" | "instance_variables"
| "instance_variable_get" | "instance_variable_set"
| "methods" | "method_missing"
);
Dynamic Methods: define_method()
Create or modify methods at runtime. Essential for building DSLs and dynamic proxies. Methods are defined on the instance's class, affecting all instances of that class.
Soli
class Foo { }
let foo = Foo.new()
# Define a method with no arguments
foo.define_method("greet", || { "Hello!" })
foo.greet() # => "Hello!"
# Define a method with arguments
foo.define_method("add", |a, b| { a + b })
foo.add(1, 2) # => 3
# Methods are defined on the class, so all instances see it:
let foo2 = Foo.new()
foo2.greet() # => "Hello!"
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs:
// define_method adds to Class.methods (now using RefCell for interior mutability)
// The Function comes from a lambda passed as argument
// Methods are stored in the class's methods HashMap
// Note: Class-level define_method (Foo.define_method) is not yet implemented
Method Aliasing: alias_method()
Create an alias for an existing method. Useful for AOP-style wrapping and maintaining backward compatibility.
Soli
class Foo {
fn greet(name) {
return "Hello, " + name + "!";
}
}
let foo = Foo.new()
foo.alias_method("say_hello", "greet")
foo.say_hello("World") # => "Hello, World!"
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs:
// alias_method copies the method function to the new name
class.methods.borrow_mut().insert(new_name, method.clone());
class.all_methods_cache.borrow_mut().take();
Inheritance Hook: inherited()
Called automatically when a class inherits from another class. Define static fn inherited(child) on a class to be notified when subclasses are created.
Soli
class Parent {
static fn inherited(child) {
print("Child class created: " + child.to_string())
}
}
class Child < Parent { }
# Output: "Child class created: <class Child>"
Implementation Notes
Implemented in src/interpreter/executor/statements.rs:
// After class creation, call inherited hook if superclass has one
if let Some(inherited_func) = superclass.find_static_method("inherited") {
self.call_function(&inherited_func, vec![Value::Class(class_rc.clone())])?;
}
if let Some(inherited_native) = superclass.find_native_static_method("inherited") {
let span = Span::new(0, 0, 1, 1);
let result: Result<Value, String> =
(inherited_native.func)(vec![Value::Class(class_rc.clone())]);
result.map_err(|e| RuntimeError::new(e, span))?;
}
Contextual Evaluation: instance_eval()
Evaluate a block in the context of an instance, with this and self bound to the instance.
Soli
class Counter
count: Int = 0
end
let c = new Counter()
c.instance_eval {
this.count = 42
}
c.count # => 42
# Can also read values
let val = c.instance_eval { this.count }
val # => 42
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs as an instance method:
// instance_eval executes a block with:
// - 'this' bound to the instance
// - 'self' bound to the instance (alias for this)
// - closure environment preserved
// Note: class_eval requires modifying class methods at runtime,
// which requires architectural changes to Class.methods
Module Hooks: included, extended, prepended
Callbacks invoked when a module is included in a class, extended onto an object, or prepended before a class.
Soli (Planned)
module Serializable
static def included(base)
base.define_method("to_json", def {
JSON.encode(this.as_hash)
})
base.define_method("as_hash", def {
let result = {}
for v in this.instance_variables
result[v] = this.instance_variable_get(v)
end
result
})
end
end
class User
include Serializable
end
User.new.to_json
Implementation Notes
Hook into the include statement resolution in the parser/interpreter. The included static method would be called automatically:
// When processing `include ModuleName`:
// 1. Load the module (existing behavior)
// 2. Check if Module has static method "included"
// 3. If yes, call Module.included(including_class)
// 4. The hook can modify the class's methods, etc.
Introspection API
Methods for discovering an object's structure at runtime.
| Method | Returns | Description |
|---|---|---|
methods() |
Array<String> | All accessible method names |
public_methods() |
Array<String> | Public method names |
private_methods() |
Array<String> | Private method names |
instance_variables() |
Array<String> | Instance variable names (including @) |
instance_variable_get(name) |
Any | Get instance variable value |
instance_variable_set(name, value) |
Any | Set instance variable value |
class_variables() |
Array<String> | Class variable names |
constants() |
Hash | Constants defined in class/module |
Example Usage
class User
name: String
email: String
def greet
"Hello!"
end
end
let user = User.new
user.instance_variables # => ["@name", "@email"]
user.methods # => ["greet", "to_s", ...]
user.respond_to?("greet") # => true
Implementation Notes
These use existing data structures in Class and Instance:
// Instance.fields: HashMap
// Class.methods: HashMap>
// Class.native_methods: HashMap>
// methods() would iterate and flatten from
// inheritance chain using find_method
Real-World Example: Dynamic Finders
A practical application combining method_missing and respond_to? to create Rails-style dynamic finders.
class ApplicationRecord
static def method_missing(method_name)
if (method_name.starts_with("find_by_"))
let field = method_name.sub("find_by_", "")
return "Finding by " + field
end
return "Method '" + method_name + "' was called"
end
end
class User < ApplicationRecord
# Inherit dynamic finder capability
end
# These all work automatically:
User.find_by_name("Alice") # => "Finding by name"
User.find_by_email("[email protected]") # => "Finding by email"
User.find_by_name_and_status("Bob", "active") # => "Finding by name_and_status"
Implementation Priority
| Priority | Feature | Rationale |
|---|---|---|
| High | respond_to?() | Foundation for safe dynamic dispatch, minimal API surface |
| High | send() | Enables dynamic method calls without method_missing dependency |
| High | method_missing() | Core expressiveness, enables DSLs and dynamic proxies |
| High | define_method() | Implemented - defines methods on instance's class |
| Medium | alias_method() | Implemented - creates method aliases |
| Medium | inherited() | Implemented - called when class inherits from another |
| Low | included / extended / prepended | Module hooks for mixins (not yet implemented) |