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

Class Method Summary collapse

Class Attribute Details

.cache_dirObject

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

.registryObject

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