Skip to content

Instantly share code, notes, and snippets.

@judofyr
Created July 22, 2010 10:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save judofyr/485811 to your computer and use it in GitHub Desktop.
Save judofyr/485811 to your computer and use it in GitHub Desktop.
## Ruby Quiz #666
module CompileSite; end
def compile(name, source)
CompileSite.class_eval <<-RUBY
def #{name}(locals)
Thread.current[:tilt_vars] = [self, locals]
class << self
this, locals = Thread.current[:tilt_vars]
this.instance_eval do
#{source}
end
end
end
RUBY
end
def render(name, scope, locals, &blk)
scope.__send__(name, locals, &blk)
end
module Helper
A = "A"
end
class Scope
include CompileSite
include Helper
B = "B"
end
class ScopeB < Scope
B = 'C'
end
compile('hello', <<-RUBY)
if locals.nil?
yield
else
[locals, A, B, yield, render(:hello, self, nil) { 'Y2' }, yield, locals].join(" ")
end
RUBY
CompileSite.instance_method(:hello)
res = render(:hello, Scope.new, "L") { "Y1" }
res2 = render(:hello, ScopeB.new, "L") { "Y1" }
if res == "L A B Y1 Y2 Y1 L" and res2 == "L A C Y1 Y2 Y1 L"
puts "YOU DID IT!"
else
puts "Failed:", res, res2
end
@judofyr
Copy link
Author

judofyr commented Jul 22, 2010

(I've updated the gist so it's closer to the original Tilt code)

raggi: why does the method have to be defined there? this seems like a violation of rubys constant lookup, CompileSite -> Helper

raggi: i vote for the method being defined on Scope so that it has "normal" semantics

You'll have to look at this in the context of template engines:

A fast template engine will compile the template into plain Ruby. For instance: Hello <%= world %> could be compiled into "Hello #{world}". In order to evaluate such a template you could have something like this:

class Template
  def render(scope, locals)
    # You need the Kernel.eval because of a bug in JRuby
    scope.instance_eval { Kernel.eval(@compiled_source, binding) }
  end
end

This passes every test above. BUT: it's quite slow because Ruby has to parse the string every time. This can be fixed by defining it as method instead (and then render will call that method). But we still want it to be able to pass in any scope you want: render("hello"), render(Request.new) and render(Object.new) should still work. In order to do that we had two choices: define the method under Object (which is available everywhere) or define the method under Tilt::CompileSite (which you can include if you want fast rendering). And we don't want to pollute Object with tons of methods.

Currently, render basically looks like this:

class Template
  def render(scope, locals, &blk)
    if scope.respond_to?(:__tilt__) # this method is defined in CompileSite
      compile! unless compiled?
      scope.__send__(method_name, locals, &blk)
    else
      # instance_eval version above
    end
  end
end

This means that if you simply include Tilt::CompileSite in your class, render(YourClass.new) will evaluate several times faster.

So even though we define the method under Tilt::CompileSite, we want it to work like it was defined under whatever scope you pass in to #render. My first patch to fix the constant lookup wasn't quite optimal, because it ran the code under the constant scope of the first render call, so render(Foo.new) + render(Bar.new) would both use Foo's constant scope. It wasn't perfect, but it was certainly better than having constant lookup relative to Tilt::CompileSite.

Your suggestion to define the method under each scope has a few disadvantages:

  • It would need to define a separate method each time you render it under a different scope.
  • If you render it under Object.new it will actually define the method on Object and thus pollute the namespace.

@raggi
Copy link

raggi commented Jul 22, 2010

As far as defining a method on each scope class, for the most part, in real world scenarios, that wouldn't bother me, it'd get defined for each 'controller' that renders a given template. To me that's relatively reasonable, although I can see for something that was partial heavy, this could bloat.

I think this latest solution using instance eval in the metaclass is the better one out of the lot. The problem is, it bugs out on JRuby, RBX, and 1.9.1. JRuby has issues with ScopeB, it looks up Scope::B instead of ScopeB::B. RBX and 1.9.1 raise local jump errors because the yield context is dropped in the metaclass. I get the feeling Evans going to hate this... ;-)

Interesting stuff :-)

@judofyr
Copy link
Author

judofyr commented Jul 22, 2010

I should also mention that it currently defines different methods depending on the locals you pass in. It actually "unrolls" the locals so the source actually looks like this:

foo = locals[:foo]
bar = locals[:bar]
#{original_source}

So if we define the method directly on the scope, it means that a total of L * S methods will be defined (where L is the number of variations of lvars and S is the number of variations of scope).

@raggi
Copy link

raggi commented Jul 22, 2010

Yeah, I had a feeling you'd simplified that from the original. That's fine... :-)

Good work on the bug reports!

@judofyr
Copy link
Author

judofyr commented Jul 22, 2010

JRuby is weird. This works:

class Base
  class_eval <<-RUBY
    def hello
      class << self
        yield
      end
    end
  RUBY
end

p Base.new.hello { 123 }

But this does not:

class Base
  #class_eval <<-RUBY
    def hello
      class << self
        yield
      end
    end
  #RUBY
end

p Base.new.hello { 123 }

@judofyr
Copy link
Author

judofyr commented Jul 26, 2010

Yay! The bugs in Rubinius are now closed :-)

@raggi
Copy link

raggi commented Jul 26, 2010

awesome

@rtomayko
Copy link

rtomayko commented Aug 1, 2010

Merged. Passes all tests on 1.8.7. I'm pulling down the latest 1.9.1 now. Review appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment