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

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)

See Also