diff --git a/Rakefile b/Rakefile index 889fc722..99a06aa2 100755 --- a/Rakefile +++ b/Rakefile @@ -53,6 +53,21 @@ task :test do Rake::Task['integration_test'].reenable Rake::Task['integration_test'].invoke end + + if RUBY_ENGINE == 'ruby' + Rake::Task['test_compile'].invoke + end +end + +task :test_compile do + ENV['LIQUID_COMPILE'] = '1' + ENV['LIQUID_PARSER_MODE'] = 'lax' + Rake::Task['integration_test'].reenable + Rake::Task['integration_test'].invoke + + ENV['LIQUID_PARSER_MODE'] = 'strict' + Rake::Task['integration_test'].reenable + Rake::Task['integration_test'].invoke end task(gem: :build) diff --git a/lib/liquid/compile.rb b/lib/liquid/compile.rb new file mode 100644 index 00000000..b32f8d52 --- /dev/null +++ b/lib/liquid/compile.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'pry' + +module Liquid + class BlockBody + def render_to_output_buffer(context, output) + ruby = +"->(__context, __output, __nodes) {\n" + compile(ruby) + ruby << "\n}" + + nodes = nodelist.each_with_object({}) do |node, hash| + hash[node.object_id] = node + end + + RubyVM::InstructionSequence + .compile(ruby) + .eval + .call(context, output, nodes) + + output + end + + def compile(ruby) + ruby << "__context.resource_limits.increment_render_score(#{nodelist.length})\n" + ruby << "catch(:__interrupt) do\n" + nodelist.each do |node| + if node.instance_of?(String) + ruby << "__output << #{node.inspect}\n" + else + ruby << "begin\n" + if node.line_number.is_a?(Integer) + ruby << "__line_number = #{node.line_number}\n" + else + ruby << "__line_number = nil\n" + end + ruby << "__is_blank = #{!node.instance_of?(Variable) && node.blank?}\n" + ruby << "__node = __nodes[#{node.object_id}]\n" + if node.respond_to?(:compile) + node.compile(ruby) + else + ruby << "__node.render_to_output_buffer(__context, __output)\n" + end + ruby << <<~RUBY + rescue => __exc + case __exc + when Liquid::MemoryError + raise + when Liquid::UndefinedVariable, Liquid::UndefinedDropMethod, Liquid::UndefinedFilter + __context.handle_error(__exc, __line_number) + else + __error_message = __context.handle_error(__exc, __line_number) + unless __is_blank + __output << __error_message + end + end + end + + throw :__interrupt if __context.interrupt? + RUBY + end + ruby << "__context.resource_limits.increment_write_score(__output)\n" + end + ruby << "end\n" + end + end +end diff --git a/performance/profile.rb b/performance/profile.rb index 70740778..58cf43ea 100644 --- a/performance/profile.rb +++ b/performance/profile.rb @@ -3,6 +3,16 @@ require 'stackprof' require_relative 'theme_runner' +if ENV['LIQUID_C'] == '1' + puts "-- LIQUID C" + require 'liquid/c' +end + +if ENV['LIQUID_COMPILE'] == '1' + puts "-- COMPILED" + require 'liquid/compile' +end + Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first profiler = ThemeRunner.new profiler.run diff --git a/test/test_helper.rb b/test/test_helper.rb index 874dd8f4..50a9e769 100755 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -20,6 +20,11 @@ if ENV['LIQUID_C'] == '1' require 'liquid/c' end +if ENV['LIQUID_COMPILE'] == '1' + puts "-- COMPILED" + require 'liquid/compile' +end + if Minitest.const_defined?('Test') # We're on Minitest 5+. Nothing to do here. else