From 0de722968c7e1997a2e85f7564025945da45397f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Cournoyer?= Date: Wed, 7 Oct 2020 13:31:59 -0400 Subject: [PATCH] Introduce the AST Optimizer ... that can only optimize one thing: combine `| append: 'a' | append: 'b'` into a single `| append_all: ['a', 'b']` filter call. This avoids all the intermediate allocations caused by several `append`. Also introduce a new filter `append_all: [*items]`. --- lib/liquid.rb | 1 + lib/liquid/optimizer.rb | 55 +++++++++++++++++++++++++++++++++++ lib/liquid/standardfilters.rb | 4 +++ test/unit/optimizer_test.rb | 16 ++++++++++ 4 files changed, 76 insertions(+) create mode 100644 lib/liquid/optimizer.rb create mode 100644 test/unit/optimizer_test.rb diff --git a/lib/liquid.rb b/lib/liquid.rb index e10cdf6b..8fd3788f 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -84,6 +84,7 @@ require 'liquid/usage' require 'liquid/register' require 'liquid/static_registers' require 'liquid/template_factory' +require 'liquid/optimizer' # Load all the tags of the standard library # diff --git a/lib/liquid/optimizer.rb b/lib/liquid/optimizer.rb new file mode 100644 index 00000000..97bc44e7 --- /dev/null +++ b/lib/liquid/optimizer.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Liquid + class Optimizer + def optimize(node) + case node + when Liquid::Template then optimize(node.root.body) + when Liquid::Document then optimize(node.body) + when Liquid::BlockBody then optimize_block(node) + when Liquid::Variable then optimize_variable(node) + when Liquid::Assign then optimize_assign(node) + when Liquid::For then optimize_for(node) + when Liquid::If then optimize_if(node) + end + node + end + + def optimize_block(block) + block.nodelist.each { |node| optimize(node) } + end + + def optimize_variable(node) + # Turn chained `| append: "..."| append: "..."`, into a single `append_all: [...]` + if node.filters.size > 1 && node.filters.all? { |f, _| f == "append" } + node.filters = [["append_all", node.filters.map { |f, (arg)| arg }]] + end + end + + def optimize_assign(node) + optimize(node.from) + end + + def optimize_for(node) + optimize(node.collection_name) + optimize_block(node) + end + + def optimize_if(node) + node.blocks.each do |block| + optimize_condition(block) + optimize(block.attachment) + end + end + + def optimize_condition(node) + case node + when Liquid::ElseCondition + # noop + when Liquid::Condition + optimize(node.left) + optimize(node.right) if node.right + end + end + end +end \ No newline at end of file diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 0f4c9145..b4e67689 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -281,6 +281,10 @@ module Liquid input.to_s + string.to_s end + def append_all(input, *items) + input.to_s + items.join + end + def concat(input, array) unless array.respond_to?(:to_ary) raise ArgumentError, "concat filter requires an array argument" diff --git a/test/unit/optimizer_test.rb b/test/unit/optimizer_test.rb new file mode 100644 index 00000000..f3014da5 --- /dev/null +++ b/test/unit/optimizer_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'test_helper' + +class OptimizerUnitTest < Minitest::Test + include Liquid + + def test_combines_append_filters + optimizer = Optimizer.new + var = Variable.new('hello | append: "a" | append: b', ParseContext.new) + var = optimizer.optimize(var) + assert_equal([ + ['append_all', ["a", VariableLookup.new("b")]] + ], var.filters) + end +end