Using AI to generate code for a new application is a familiar workflow today. But what if an application starts as a completely blank slate, learning on the job and writing its own implementation live as you call nonexistent methods?
This concept of live-patching and zero-downtime execution isn't entirely new. Early in my career at Nortel Networks, I have seen this with PROTEL (PRocess Oriented TELepony language), a proprietary language designed for telecom switches. To achieve "five nines" (99.999%) availability, you couldn't simply take systems offline for deployments—code updates had to happen via live hot-patching. Later on, I thoroughly enjoyed similar live code reloading capabilities while working with Erlang.
Recently, as I spent more time with Ruby, its rich metaprogramming capabilities got me thinking: What if we start with an empty Ruby object, and as we call methods on it, it uses an LLM to write and evaluate its own code on the fly?
1. The Blank Slate
Let's start with a completely empty class:
class Dummy < LiveCode
end
Now, let's fire up irb (Ruby's REPL) and start interacting with our dummy object as if the methods already existed:
dummy = Dummy.new
dummy.add(1, 2)
At this point, Ruby will complain because Dummy doesn't have an add method. However, the method name (add) and its arguments (1, 2) clearly communicate our intent.
Ruby provides a built-in hook called method_missing to catch calls to undefined methods. This is where we bring in AI. To interact with our LLM provider, we'll use the excellent RubyLLM gem.
2. Dynamic Method Generation via method_missing
To make this behavior reusable across objects, we put our logic inside a LiveCode base class that Dummy inherits from.
Here is our initial LiveCode implementation:
class LiveCode
def method_missing(method_name, *args, **kwargs)
p "Missing method: #{method_name}, #{args.inspect}, #{kwargs.inspect}"
prompt = <<~PROMPT
You are a Ruby code generator. A missing method `#{method_name}` was called
with the arguments: #{args.map(&:class)}, keyword arguments: #{kwargs.keys}.
Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def #{method_name}(...)
# implementation
end
PROMPT
ruby_code = @chat.ask(prompt).content
# Evaluate the generated Ruby code directly on the instance's singleton class
singleton_class.class_eval(ruby_code)
# Re-dispatch the original method call now that it exists!
send(method_name, *args, **kwargs)
end
end
How It Works
-
Interception: When
dummy.add(1, 2)is called,method_missingintercepts the call and extracts the method name (:add) and parameter types (Integer, Integer). - LLM Prompting: We construct a prompt instructing the model to return only valid Ruby code defining the method.
-
Metaprogramming: We use
singleton_class.class_eval(ruby_code)to inject the generated method into our object at runtime. -
Re-dispatch: Finally,
send(method_name, ...)invokes the newly defined method seamlessly!
3. Seeing It in Action
Let's test this in IRB:
irb(main):003> dummy.add(1, 2)
"Missing method: add, [1, 2], {}"
"Prompt: You are a Ruby code generator. A missing method `add` was called
with the arguments: [Integer, Integer], keyword arguments: [].
Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def add(...)
# implementation
end
"
"Code: def add(a, b)
a + b
end"
=> 3
In real-time, the LLM synthesized def add(a, b); a + b; end, registered it on dummy, executed it, and returned 3. Subsequent calls to dummy.add(1, 2) will execute instantly without hitting method_missing again!
4. Adding Context: State and Inter-Method Dependencies
A real object has multiple methods that need to share state via instance variables and interact with one another. To enable this, our LLM needs context about existing instance variables, their types, and previously generated methods.
We introduce a helper method llm_context to capture this state:
def llm_context
context = "# Current Instance Variables and Types:\n"
if instance_variables.empty?
context += "(No instance variables yet)\n"
else
instance_variables.each do |name|
value = instance_variable_get(name)
context += " - #{name}: #{value.class.name}\n"
end
end
context += "# Previously Generated Methods:\n"
if @_generated_methods.nil? || @_generated_methods.empty?
context += "(No generated methods yet)\n"
else
@_generated_methods.each do |name, code|
context += "#{code}\n"
end
end
context
end
We then update our prompt to include this rich context:
prompt = <<~PROMPT
You are a Ruby code generator. A missing method `#{method_name}` was called
with the arguments: #{args.map(&:class)}, keyword arguments: #{kwargs.keys}.
Write ONLY the valid Ruby code to define `#{method_name}`. Ensure it works
well with the existing instance variables and previously generated methods.
#{llm_context}
Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def #{method_name}(...)
# implementation
end
PROMPT
5. Stateful Walkthrough: Setters & Getters
Let's test setting a property and then retrieving it in a subsequent call.
Step 1: Setting a Value
irb(main):003> dummy.name = "Onur"
"Missing method: name=, [\"Onur\"], {}"
"Prompt: You are a Ruby code generator. A missing method `name=` was called
with the arguments: [String], keyword arguments: [].
Write ONLY the valid Ruby code to define `name=`. Ensure it works
well with the existing instance variables and previously generated methods.
# Current Instance Variables and Types:
- @chat: RubyLLM::Chat
# Previously Generated Methods:
(No generated methods yet)
Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def name=(...)
# implementation
end
"
"Code: def name=(value)
@name = value
end"
=> "Onur"
Calling dummy.name = "Onur" dynamically created the setter method name=(value) which initialized the @name instance variable.
Step 2: Reading the Value
irb(main):004> dummy.name
"Missing method: name, [], {}"
"Prompt: You are a Ruby code generator. A missing method `name` was called
with the arguments: [], keyword arguments: [].
Write ONLY the valid Ruby code to define `name`. Ensure it works
well with the existing instance variables and previously generated methods.
# Current Instance Variables and Types:
- @chat: RubyLLM::Chat
- @_generated_methods: Hash
- @name: String
# Previously Generated Methods:
def name=(value)
@name = value
end
Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def name(...)
# implementation
end
"
"Code: def name
@name
end"
=> "Onur"
Because our prompt included @name: String and the previously defined name=(value) method, the LLM understood the context and generated the exact matching getter method def name; @name; end.
What's Next?
This experiment demonstrates how easily Ruby's dynamic nature combines with LLMs to build self-assembling objects. There are a few natural next steps for expanding this idea—such as persisting the generated code to disk, adding sandboxing/security checks, or enabling feedback loops to auto-fix runtime errors.
Source Code & Examples
The complete working source code for this post is available in the site repository:
Top comments (0)