Module: Mana::Compiler
- Defined in:
- lib/mana/compiler.rb
Overview
Compiler for mana def — LLM generates method implementations on first call,
caches them as real .rb files, and replaces the method with native Ruby.
Usage:
mana def fibonacci(n)
~"return an array of the first n Fibonacci numbers"
end
fibonacci(10) # first call → LLM generates code → cached → executed
fibonacci(20) # subsequent calls → pure Ruby, zero API overhead
Mana.source(:fibonacci) # view generated source
Class Attribute Summary collapse
-
.cache_dir ⇒ Object
Cache directory for generated .rb files.
Class Method Summary collapse
-
.cache_file_path(method_name, owner = nil, source_file: nil) ⇒ Object
Path to the cache file for a method.
-
.clear! ⇒ Object
Clear all cached files and registry.
-
.compile(owner, method_name) ⇒ Object
Compile a method: wrap it so first invocation triggers LLM code generation.
-
.generate(method_name, params_desc, prompt) ⇒ Object
Generate Ruby method source via LLM.
-
.registry ⇒ Object
Registry of compiled method sources: { "ClassName#method" => source_code }.
-
.source(method_name, owner: nil) ⇒ Object
Get the generated source for a compiled method.
-
.write_cache(method_name, source, owner = nil, prompt_hash: nil, prompt: nil, source_file: nil) ⇒ Object
Write generated code to a cache file, return the path.
Class Attribute Details
.cache_dir ⇒ Object
Cache directory for generated .rb files
27 28 29 |
# File 'lib/mana/compiler.rb', line 27 def cache_dir @cache_dir || File.join(".ruby-mana", "cache") end |
Class Method Details
.cache_file_path(method_name, owner = nil, source_file: nil) ⇒ Object
Path to the cache file for a method. Includes source file path for uniqueness: lib_foo_calculate.rb Build the cache file path for a method. Prefers source-file-based naming for uniqueness; falls back to owner class name.
144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/mana/compiler.rb', line 144 def cache_file_path(method_name, owner = nil, source_file: nil) parts = [] if source_file # Convert path relative to pwd: lib/foo.rb -> lib_foo rel = source_file.sub("#{Dir.pwd}/", "").sub(/\.rb$/, "") parts << rel.tr("/", "_") elsif owner && owner != Object # Use underscored class name when source file is unavailable parts << underscore(owner.name) end parts << method_name.to_s File.join(cache_dir, "#{parts.join('_')}.rb") end |
.clear! ⇒ Object
Clear all cached files and registry
172 173 174 175 |
# File 'lib/mana/compiler.rb', line 172 def clear! FileUtils.rm_rf(cache_dir) if Dir.exist?(cache_dir) @registry = {} end |
.compile(owner, method_name) ⇒ Object
Compile a method: wrap it so first invocation triggers LLM code generation. On subsequent calls, the generated Ruby code is loaded from cache (zero API cost).
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/mana/compiler.rb', line 41 def compile(owner, method_name) original = owner.instance_method(method_name) compiler = self key = registry_key(method_name, owner) # Detect method visibility before we replace it visibility = if owner.private_method_defined?(method_name) :private elsif owner.protected_method_defined?(method_name) :protected else :public end # Read the prompt from the original method body (the ~"..." string) prompt = extract_prompt(original) # Build parameter signature for the generated method params_desc = describe_params(original) # Cache filename based on source file + method name source_file = original.source_location&.first # Include gem version, Ruby version, and sibling function signatures so cache # auto-invalidates when the gem upgrades, Ruby upgrades, or dependency functions change. sibling_methods = begin Mana::Introspect.methods_from_file(source_file) .reject { |m| m[:name] == method_name.to_s } .map { |m| "#{m[:name]}(#{m[:params].join(',')})" } .sort.join(";") rescue "" end prompt_hash = Digest::SHA256.hexdigest("#{Mana::VERSION}:#{RUBY_VERSION}:#{method_name}:#{params_desc}:#{prompt}:#{sibling_methods}")[0, 16] cache_path = cache_file_path(method_name, owner, source_file: source_file) # Load from cache if file exists and prompt hash matches if File.exist?(cache_path) first_line = File.open(cache_path, &:readline) rescue "" if first_line.include?(prompt_hash) cached = File.read(cache_path) generated = cached.lines.reject { |l| l.start_with?("#") }.join.strip compiler.registry[key] = generated v, $VERBOSE = $VERBOSE, nil owner.class_eval(generated, cache_path, 1) owner.send(visibility, method_name) unless visibility == :public $VERBOSE = v return end # Prompt changed — cache is stale, will regenerate on first call end # Replace the method with a lazy wrapper that generates code on first call old_verbose, $VERBOSE = $VERBOSE, nil p_hash = prompt_hash # capture for closure p_text = prompt # capture for closure src_file = source_file # capture for closure owner.define_method(method_name) do |*args, **kwargs, &blk| # Generate implementation via LLM generated = compiler.generate(method_name, params_desc, prompt) # Write to cache file for future runs cache_path = compiler.write_cache(method_name, generated, owner, prompt_hash: p_hash, prompt: p_text, source_file: src_file) # Store in registry so Mana.source() can retrieve it compiler.registry[key] = generated # Define the method on the correct owner (not Object) via class_eval target_owner = owner v, $VERBOSE = $VERBOSE, nil target_owner.class_eval(generated, cache_path, 1) target_owner.send(visibility, method_name) unless visibility == :public $VERBOSE = v # Call the now-native method (this wrapper never runs again) send(method_name, *args, **kwargs, &blk) end # Restore original visibility on the wrapper method owner.send(visibility, method_name) unless visibility == :public $VERBOSE = old_verbose end |
.generate(method_name, params_desc, prompt) ⇒ Object
Generate Ruby method source via LLM. Uses an isolated binding so LLM cannot see Compiler internals.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/mana/compiler.rb', line 124 def generate(method_name, params_desc, prompt) engine_prompt = "Write a Ruby method definition `def #{method_name}(#{params_desc})` that: #{prompt}. " \ "Return ONLY the complete method definition (def...end), no explanation. " \ "Store the code as a string in <code>" # Create isolated binding with only `code` variable visible. # Use eval to avoid "assigned but unused variable" parse-time warning. isolated = Object.new.instance_eval { eval("code = nil; binding") } Mana::Engine.new(isolated).execute(engine_prompt) code = isolated.local_variable_get(:code) # LLM may return literal \n instead of real newlines — unescape them code = code.gsub("\\n", "\n").gsub("\\\"", "\"").gsub("\\'", "'") if code.is_a?(String) code end |
.registry ⇒ Object
Registry of compiled method sources: { "ClassName#method" => source_code }
22 23 24 |
# File 'lib/mana/compiler.rb', line 22 def registry @registry ||= {} end |
.source(method_name, owner: nil) ⇒ Object
Get the generated source for a compiled method
34 35 36 37 |
# File 'lib/mana/compiler.rb', line 34 def source(method_name, owner: nil) key = registry_key(method_name, owner) registry[key] end |
.write_cache(method_name, source, owner = nil, prompt_hash: nil, prompt: nil, source_file: nil) ⇒ Object
Write generated code to a cache file, return the path
159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/mana/compiler.rb', line 159 def write_cache(method_name, source, owner = nil, prompt_hash: nil, prompt: nil, source_file: nil) FileUtils.mkdir_p(cache_dir) path = cache_file_path(method_name, owner, source_file: source_file) header = "# Auto-generated by ruby-mana v#{Mana::VERSION} | ruby #{RUBY_VERSION} | prompt_hash: #{prompt_hash}\n" if prompt prompt.to_s.each_line { |line| header += "# prompt: #{line.rstrip}\n" } end header += "# frozen_string_literal: true\n\n" File.write(path, "#{header}#{source}\n") path end |