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
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_"))
field = method_name.sub("find_by_", "")
return "Finding person by " + field
end
return "Method '" + method_name + "' was called"
end
end
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
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 { }
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:
foo2 = Foo.new()
foo2.greet() # => "Hello!"
Implementation Notes
Implemented in src/interpreter/executor/access/member.rs:
// define_method writes to Class.methods (RefCell for interior mutability).
// Class-level form (Foo.define_method) is supported alongside the
// instance-level form (foo.define_method).
//
// For primitive-tagged classes (Int, Float, String, Array, Hash, etc.)
// writes are routed to a per-type user-method overlay instead, since
// primitive dispatch in member.rs / vm.rs does not consult Class.methods.
// See `src/interpreter/executor/calls/user_methods.rs`.
Extending primitive types
Int, Float, Bool, Decimal, String, Array, Hash, Symbol, and Null can be reopened with define_method / alias_method. User methods are checked before built-ins and shadow them on collision.
Soli
Int.define_method("doubled", fn() { this * 2 })
Int.define_method("times_n", fn(n) { this * n })
3.doubled # => 6 (zero-arg auto-invokes without parens)
3.doubled() # => 6
4.times_n(5) # => 20
String.define_method("shout", fn() { this + "!!!" })
"hi".shout() # => "hi!!!"
Array.define_method("second", fn() { this[1] })
[10, 20, 30].second() # => 20
# alias_method works intra-type:
String.define_method("yell", fn() { this + "!" })
String.alias_method("shout", "yell")
Precedence and performance
Lookup order on a primitive value: user methods → built-in methods → (for Hash) literal hash-key fallback. A user method on Hash shadows the literal-key access, matching Ruby's monkey-patching semantics.
Dispatch is gated by a single Relaxed atomic load. When no user methods have been registered for any primitive type, every call short-circuits in one cycle — no measurable overhead on the hot path. Storage is thread_local!; each worker thread registers its own copy when models load.
Named scopes: scope(name, fn)
scope is a class-body DSL — same shape as validates, has_many, before_save. The class is auto-prepended to the call, and this inside the closure is bound to a fresh QueryBuilder for the model.
Soli
class Post < Model
scope("published", fn() { this.where("status = @s", { "s": "published" }) })
scope("recent", fn() { this.order("created_at", "desc").limit(10) })
end
# Accessing the scope name invokes the closure and returns a QueryBuilder:
let posts = Post.published.where("author_id = @id", { "id": 42 }).all
Implementation Notes
The closure returns the (possibly refined) QueryBuilder. Scope storage is per-thread (Rc<Function> is !Send and can't go in the process-global MODEL_REGISTRY); each worker registers scopes when it loads model files. See src/interpreter/builtins/model/scopes.rs.
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 + "!";
}
}
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
c = new Counter()
c.instance_eval {
this.count = 42
}
c.count # => 42
# Can also read values
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 {
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
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_"))
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) |