ESC
Type to search...
S
Soli Docs

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
inst.class.methods.insert(method_name, function_rc); // Class/static method: class.methods.insert(method_name, function_rc); // Important: Invalidate the all_methods_cache when adding methods // See Class::ensure_methods_cached() - it rebuilds lazily on next access

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)

See Also