Dynamic Dispatch
send is the method that isn't supposed to compile. Everything else Rails does dynamically — associations, callbacks, scopes, the whole metaprogramming apparatus — is elaborate, but it resolves: given the whole program, you can follow each one to the concrete method it names. send is different in kind. obj.send(name) calls whatever method name holds at runtime, and if name is a value that arrived from outside, there is no method to resolve to at compile time. That's fatal for an ahead-of-time compiler, which has to know every call's target to lay down code for it — and it's a problem for every strict target, because reflective dispatch is exactly the thing whole-program analysis can't see through.
So the honest expectation, walking into a real application, is that send is where the transpile stops.
Then you look at what the application actually does with it. lobste.rs — a real, deployed Rails app, ten times the size of the blog demo I'd been measuring against — reaches send with a dynamic name in exactly six places , and those six fall into three shapes . And in every one of them, the name isn't computed from the outside world. It's drawn from a collection the author wrote down a few lines up. The reflection is real, but the name set is finite, local, and — the word that matters — provable.
Which turns the problem inside out. If you can prove the complete set of names a send can dispatch to, you don't need reflection at all. You can rewrite it into the dispatch table the programmer wrote in longhand.
What the rewrite does
Here is the richest of the three shapes, verbatim from lobste.rs' Story model — the as_json serializer, which walks a spec array and calls each entry as a method:
def as_json(options = {})<br>h = [<br>:short_id, :short_id_url, :created_at, :title, :url, :score, :score, :flags,<br>{ :comment_count => :comments_count },<br>{ :description => :markeddown_description },<br>{ :description_plain => :description },<br>:comments_url,<br>{ :submitter_user => :user },<br>{ :tags => self.tags.map(&:tag).sort },
if options && options[:with_comments]<br>h.push(:comments => options[:with_comments])<br>end
js = {}<br>h.each do |k|<br>if k.is_a?(Symbol)<br>js[k] = self.send(k)<br>elsif k.is_a?(Hash)<br>if k.values.first.is_a?(Symbol)<br>js[k.keys.first] = self.send(k.values.first)<br>else<br>js[k.keys.first] = k.values.first<br>end<br>end<br>end<br>js<br>end<br>There are two dynamic sends here — self.send(k) and self.send(k.values.first) — and neither name is knowable in isolation. But h is a local array literal, grown only by push, iterated with the block variable k. Every symbol it can hold is written right there. A pass can read the literal, collect the reachable names, and rewrite each send into a case over exactly those names:
js[k] = case k<br>when :short_id then self.short_id<br>when :short_id_url then self.short_id_url<br>when :created_at then self.created_at<br>when :title then self.title<br>when :url then self.url<br>when :score then self.score<br>when :flags then self.flags<br>when :comments_url then self.comments_url<br>else raise "dynamic send: method not in the statically enumerated set"<br>end<br>Two things about that else arm. First, it's not decoration — it's the whole reason the rewrite is sound rather than merely plausible. send raises NoMethodError when handed a name the object doesn't respond to; the wildcard arm preserves that exact failure mode. If a symbol ever reaches this case that the pass didn't foresee, the program raises — loudly, at the same place Ruby would have — instead of silently returning nil. The rewrite is allowed to be surprised; it is never allowed to be quietly wrong.
Second, note what didn't happen: self.tags.map(&:tag).sort and options[:with_comments] are hash values on the non-symbol path, not method names, and they contribute no arms. They're data on every real execution — a runtime symbol arriving from one of them would hit the raising arm, which is the correct thing to do with a case that genuinely can't be proven. CRuby runs the rewritten method identically: on the benchmark, /hottest's JSON comes out byte-for-byte the same (all 16369 of them) through the rewritten serializer.
Three disguises for a finite set
The as_json walk is the elaborate case. The other two show how differently a provable name set can hide.
The plainest is a literal array mapped directly — Search#to_url_params:
[:q, :what, :order].map { |p| "#{p}=#{CGI.escape(self.send(p).to_s)}" }<br>The block variable ranges over three symbols spelled out on the same line. Three arms, done.
The subtle one is FlaggedCommenters, where the name set never appears as a symbol at all:
length = time_interval(interval)<br>@period = length[:dur].send(length[:intv].downcase).ago<br>The method name is length[:intv].downcase — a string, lowercased, pulled out of a hash. To prove that set you have to follow time_interval into a helper whose every return is a hash literal, and whose :intv value is drawn from a frozen constant table:
TIME_INTERVALS = { "h" => "Hour", "d" => "Day", "w" =>...