From 560d2ce9d415631f7e73b13462052cadddfbe528 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Wed, 31 Dec 2025 12:20:26 -0400 Subject: [PATCH] Update compilers to use LR.method() calls Replace inline helper calls with LR runtime methods: - __to_s__() -> LR.to_s() - __to_number__() -> LR.to_number() - __to_integer__() -> LR.to_integer() - __truthy__() -> LR.truthy?() - __output_value__() -> LR.output() - __lookup__() -> LR.lookup() Filter compiler now generates calls like: - LR.escape_html(), LR.url_encode(), LR.base64_encode() - LR.truncate(), LR.truncatewords(), LR.slice() - LR.default(), LR.date() This reduces generated code size significantly since helpers are defined once in the pre-loaded runtime. --- lib/liquid/compile/condition_compiler.rb | 2 +- lib/liquid/compile/expression_compiler.rb | 12 +-- lib/liquid/compile/filter_compiler.rb | 99 ++++++++++---------- lib/liquid/compile/tags/cycle_compiler.rb | 2 +- lib/liquid/compile/tags/for_compiler.rb | 4 +- lib/liquid/compile/tags/tablerow_compiler.rb | 6 +- lib/liquid/compile/variable_compiler.rb | 2 +- 7 files changed, 62 insertions(+), 65 deletions(-) diff --git a/lib/liquid/compile/condition_compiler.rb b/lib/liquid/compile/condition_compiler.rb index 1c725d95..35654b04 100644 --- a/lib/liquid/compile/condition_compiler.rb +++ b/lib/liquid/compile/condition_compiler.rb @@ -66,7 +66,7 @@ module Liquid # If no operator, just check truthiness if op.nil? left_expr = ExpressionCompiler.compile(left, compiler) - return "__truthy__(#{left_expr})" + return "LR.truthy?(#{left_expr})" end # Compile left and right expressions diff --git a/lib/liquid/compile/expression_compiler.rb b/lib/liquid/compile/expression_compiler.rb index 7ef5d259..d29dd77f 100644 --- a/lib/liquid/compile/expression_compiler.rb +++ b/lib/liquid/compile/expression_compiler.rb @@ -63,16 +63,16 @@ module Liquid lookup.lookups.each_with_index do |key, index| if key.is_a?(VariableLookup) || key.is_a?(RangeLookup) # Dynamic key like foo[expr] - base = "__lookup__.call(#{base}, #{compile(key, compiler)})" + base = "LR.lookup(#{base}, #{compile(key, compiler)}, __context__)" elsif key.is_a?(Integer) # Numeric index like foo[0] - base = "__lookup__.call(#{base}, #{key})" + base = "LR.lookup(#{base}, #{key}, __context__)" elsif key.is_a?(String) - # Always use __lookup__ which tries key access first, + # Always use LR.lookup which tries key access first, # then falls back to method call for command methods (first, last, size) - base = "__lookup__.call(#{base}, #{key.inspect})" + base = "LR.lookup(#{base}, #{key.inspect}, __context__)" else - base = "__lookup__.call(#{base}, #{compile(key, compiler)})" + base = "LR.lookup(#{base}, #{compile(key, compiler)}, __context__)" end end @@ -88,7 +88,7 @@ module Liquid end_expr = compile(range.end_obj, compiler) # Convert to integers and create range - "(__to_integer__(#{start_expr})...__to_integer__(#{end_expr})).to_a" + "(LR.to_integer(#{start_expr})...LR.to_integer(#{end_expr})).to_a" end # Compile a method literal (blank/empty) diff --git a/lib/liquid/compile/filter_compiler.rb b/lib/liquid/compile/filter_compiler.rb index bc5c9284..50b82633 100644 --- a/lib/liquid/compile/filter_compiler.rb +++ b/lib/liquid/compile/filter_compiler.rb @@ -10,129 +10,127 @@ module Liquid # Standard filters that map directly to Ruby methods or simple expressions SIMPLE_FILTERS = { 'size' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:size) ? #{input}.size : 0)" }, - 'downcase' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).downcase" }, - 'upcase' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).upcase" }, - 'capitalize' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).capitalize" }, - 'strip' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).strip" }, - 'lstrip' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).lstrip" }, - 'rstrip' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).rstrip" }, - 'reverse' => ->(input, _args, _kwargs, _compiler) { "(#{input}.is_a?(Array) ? #{input}.reverse : __to_s__(#{input}).reverse)" }, + 'downcase' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).downcase" }, + 'upcase' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).upcase" }, + 'capitalize' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).capitalize" }, + 'strip' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).strip" }, + 'lstrip' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).lstrip" }, + 'rstrip' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).rstrip" }, + 'reverse' => ->(input, _args, _kwargs, _compiler) { "(#{input}.is_a?(Array) ? #{input}.reverse : LR.to_s(#{input}).reverse)" }, 'first' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:first) ? #{input}.first : nil)" }, 'last' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:last) ? #{input}.last : nil)" }, 'uniq' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:uniq) ? #{input}.uniq : #{input})" }, 'compact' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:compact) ? #{input}.compact : #{input})" }, 'flatten' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:flatten) ? #{input}.flatten : #{input})" }, 'sort' => ->(input, _args, _kwargs, _compiler) { "(#{input}.respond_to?(:sort) ? #{input}.sort : #{input})" }, - 'abs' => ->(input, _args, _kwargs, _compiler) { "__to_number__(#{input}).abs" }, - 'ceil' => ->(input, _args, _kwargs, _compiler) { "__to_number__(#{input}).ceil.to_i" }, - 'floor' => ->(input, _args, _kwargs, _compiler) { "__to_number__(#{input}).floor.to_i" }, - 'escape' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : CGI.escapeHTML(__to_s__(#{input})))" }, - 'h' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : CGI.escapeHTML(__to_s__(#{input})))" }, - 'url_encode' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : CGI.escape(__to_s__(#{input})))" }, - 'url_decode' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : CGI.unescape(__to_s__(#{input})))" }, - 'base64_encode' => ->(input, _args, _kwargs, _compiler) { "Base64.strict_encode64(__to_s__(#{input}))" }, - 'base64_decode' => ->(input, _args, _kwargs, _compiler) { "Base64.strict_decode64(__to_s__(#{input}))" }, - 'base64_url_safe_encode' => ->(input, _args, _kwargs, _compiler) { "Base64.urlsafe_encode64(__to_s__(#{input}))" }, - 'base64_url_safe_decode' => ->(input, _args, _kwargs, _compiler) { "Base64.urlsafe_decode64(__to_s__(#{input}))" }, - 'strip_html' => ->(input, _args, _kwargs, _compiler) { - "__to_s__(#{input}).gsub(%r{||}m, '').gsub(/<.*?>/m, '')" - }, - 'strip_newlines' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).gsub(/\\r?\\n/, '')" }, - 'newline_to_br' => ->(input, _args, _kwargs, _compiler) { "__to_s__(#{input}).gsub(/\\r?\\n/, \"
\\n\")" }, + 'abs' => ->(input, _args, _kwargs, _compiler) { "LR.to_number(#{input}).abs" }, + 'ceil' => ->(input, _args, _kwargs, _compiler) { "LR.to_number(#{input}).ceil.to_i" }, + 'floor' => ->(input, _args, _kwargs, _compiler) { "LR.to_number(#{input}).floor.to_i" }, + 'escape' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.escape_html(#{input}))" }, + 'h' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.escape_html(#{input}))" }, + 'url_encode' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.url_encode(#{input}))" }, + 'url_decode' => ->(input, _args, _kwargs, _compiler) { "(#{input}.nil? ? nil : LR.url_decode(#{input}))" }, + 'base64_encode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_encode(#{input})" }, + 'base64_decode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_decode(#{input})" }, + 'base64_url_safe_encode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_url_safe_encode(#{input})" }, + 'base64_url_safe_decode' => ->(input, _args, _kwargs, _compiler) { "LR.base64_url_safe_decode(#{input})" }, + 'strip_html' => ->(input, _args, _kwargs, _compiler) { "LR.strip_html(#{input})" }, + 'strip_newlines' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).gsub(/\\r?\\n/, '')" }, + 'newline_to_br' => ->(input, _args, _kwargs, _compiler) { "LR.to_s(#{input}).gsub(/\\r?\\n/, \"
\\n\")" }, }.freeze # Filters with arguments that need special handling + # All use LR.method() calls to pre-loaded runtime PARAMETERIZED_FILTERS = { 'append' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "__to_s__(#{input}) + __to_s__(#{arg})" + "LR.to_s(#{input}) + LR.to_s(#{arg})" }, 'prepend' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "__to_s__(#{arg}) + __to_s__(#{input})" + "LR.to_s(#{arg}) + LR.to_s(#{input})" }, 'plus' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "(__to_number__(#{input}) + __to_number__(#{arg}))" + "(LR.to_number(#{input}) + LR.to_number(#{arg}))" }, 'minus' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "(__to_number__(#{input}) - __to_number__(#{arg}))" + "(LR.to_number(#{input}) - LR.to_number(#{arg}))" }, 'times' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "(__to_number__(#{input}) * __to_number__(#{arg}))" + "(LR.to_number(#{input}) * LR.to_number(#{arg}))" }, 'divided_by' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "(__to_number__(#{input}) / __to_number__(#{arg}))" + "(LR.to_number(#{input}) / LR.to_number(#{arg}))" }, 'modulo' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "(__to_number__(#{input}) % __to_number__(#{arg}))" + "(LR.to_number(#{input}) % LR.to_number(#{arg}))" }, 'round' => ->(input, args, _kwargs, compiler) { if args.empty? - "__to_number__(#{input}).round.to_i" + "LR.to_number(#{input}).round.to_i" else arg = compile_arg(args[0], compiler) - "__to_number__(#{input}).round(__to_number__(#{arg}))" + "LR.to_number(#{input}).round(LR.to_number(#{arg}))" end }, 'at_least' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "[__to_number__(#{input}), __to_number__(#{arg})].max" + "[LR.to_number(#{input}), LR.to_number(#{arg})].max" }, 'at_most' => ->(input, args, _kwargs, compiler) { arg = compile_arg(args[0], compiler) - "[__to_number__(#{input}), __to_number__(#{arg})].min" + "[LR.to_number(#{input}), LR.to_number(#{arg})].min" }, 'default' => ->(input, args, kwargs, compiler) { default_val = args.empty? ? "''" : compile_arg(args[0], compiler) - allow_false = kwargs && kwargs['allow_false'] ? compile_arg(kwargs['allow_false'], compiler) : 'false' - "(if #{allow_false} then (#{input}.nil? || (#{input}.respond_to?(:empty?) && #{input}.empty?)) else (!__truthy__(#{input}) || (#{input}.respond_to?(:empty?) && #{input}.empty?)) end) ? #{default_val} : #{input}" + allow_false = kwargs && kwargs['allow_false'] ? "allow_false: #{compile_arg(kwargs['allow_false'], compiler)}" : '' + "LR.default(#{input}, #{default_val}#{allow_false.empty? ? '' : ', ' + allow_false})" }, 'split' => ->(input, args, _kwargs, compiler) { pattern = args.empty? ? "' '" : compile_arg(args[0], compiler) - "__to_s__(#{input}).split(__to_s__(#{pattern}))" + "LR.to_s(#{input}).split(LR.to_s(#{pattern}))" }, 'join' => ->(input, args, _kwargs, compiler) { glue = args.empty? ? "' '" : compile_arg(args[0], compiler) - "(#{input}.is_a?(Array) ? #{input}.map { |i| __to_s__(i) }.join(__to_s__(#{glue})) : __to_s__(#{input}))" + "(#{input}.is_a?(Array) ? #{input}.map { |i| LR.to_s(i) }.join(LR.to_s(#{glue})) : LR.to_s(#{input}))" }, 'replace' => ->(input, args, _kwargs, compiler) { string = compile_arg(args[0], compiler) replacement = args.length > 1 ? compile_arg(args[1], compiler) : "''" - "__to_s__(#{input}).gsub(__to_s__(#{string}), __to_s__(#{replacement}))" + "LR.to_s(#{input}).gsub(LR.to_s(#{string}), LR.to_s(#{replacement}))" }, 'replace_first' => ->(input, args, _kwargs, compiler) { string = compile_arg(args[0], compiler) replacement = args.length > 1 ? compile_arg(args[1], compiler) : "''" - "__to_s__(#{input}).sub(__to_s__(#{string}), __to_s__(#{replacement}))" + "LR.to_s(#{input}).sub(LR.to_s(#{string}), LR.to_s(#{replacement}))" }, 'remove' => ->(input, args, _kwargs, compiler) { string = compile_arg(args[0], compiler) - "__to_s__(#{input}).gsub(__to_s__(#{string}), '')" + "LR.to_s(#{input}).gsub(LR.to_s(#{string}), '')" }, 'remove_first' => ->(input, args, _kwargs, compiler) { string = compile_arg(args[0], compiler) - "__to_s__(#{input}).sub(__to_s__(#{string}), '')" + "LR.to_s(#{input}).sub(LR.to_s(#{string}), '')" }, 'truncate' => ->(input, args, _kwargs, compiler) { length = args.empty? ? "50" : compile_arg(args[0], compiler) ellipsis = args.length > 1 ? compile_arg(args[1], compiler) : "'...'" - var = compiler.generate_var_name("trunc") - "(lambda { |#{var}_input, #{var}_len, #{var}_ell| #{var}_str = __to_s__(#{var}_input); #{var}_ell_str = __to_s__(#{var}_ell); #{var}_l = [#{var}_len.to_i - #{var}_ell_str.length, 0].max; #{var}_str.length > #{var}_len.to_i ? #{var}_str[0, #{var}_l] + #{var}_ell_str : #{var}_str }).call(#{input}, #{length}, #{ellipsis})" + "LR.truncate(#{input}, #{length}, #{ellipsis})" }, 'truncatewords' => ->(input, args, _kwargs, compiler) { words = args.empty? ? "15" : compile_arg(args[0], compiler) ellipsis = args.length > 1 ? compile_arg(args[1], compiler) : "'...'" - "(lambda { |input, num_words, ell| words = __to_s__(input).split(' ', [num_words.to_i, 1].max + 1); words.length > [num_words.to_i, 1].max ? words[0, [num_words.to_i, 1].max].join(' ') + __to_s__(ell) : input.to_s }).call(#{input}, #{words}, #{ellipsis})" + "LR.truncatewords(#{input}, #{words}, #{ellipsis})" }, 'slice' => ->(input, args, _kwargs, compiler) { offset = compile_arg(args[0], compiler) length = args.length > 1 ? compile_arg(args[1], compiler) : "1" - "(#{input}.is_a?(Array) ? (#{input}.slice(__to_integer__(#{offset}), __to_integer__(#{length})) || []) : (__to_s__(#{input}).slice(__to_integer__(#{offset}), __to_integer__(#{length})) || ''))" + "LR.slice(#{input}, #{offset}, #{length})" }, 'map' => ->(input, args, _kwargs, compiler) { property = compile_arg(args[0], compiler) @@ -144,7 +142,7 @@ module Liquid target = compile_arg(args[1], compiler) "(#{input}.is_a?(Array) ? #{input}.select { |item| item.respond_to?(:[]) && item[#{property}] == #{target} } : [])" else - "(#{input}.is_a?(Array) ? #{input}.select { |item| item.respond_to?(:[]) && __truthy__(item[#{property}]) } : [])" + "(#{input}.is_a?(Array) ? #{input}.select { |item| item.respond_to?(:[]) && LR.truthy?(item[#{property}]) } : [])" end }, 'reject' => ->(input, args, _kwargs, compiler) { @@ -153,7 +151,7 @@ module Liquid target = compile_arg(args[1], compiler) "(#{input}.is_a?(Array) ? #{input}.reject { |item| item.respond_to?(:[]) && item[#{property}] == #{target} } : [])" else - "(#{input}.is_a?(Array) ? #{input}.reject { |item| item.respond_to?(:[]) && __truthy__(item[#{property}]) } : [])" + "(#{input}.is_a?(Array) ? #{input}.reject { |item| item.respond_to?(:[]) && LR.truthy?(item[#{property}]) } : [])" end }, 'concat' => ->(input, args, _kwargs, compiler) { @@ -170,11 +168,10 @@ module Liquid }, 'date' => ->(input, args, _kwargs, compiler) { format = compile_arg(args[0], compiler) - # This is a simplified version - full date parsing is complex - "(lambda { |input, fmt| return input if fmt.to_s.empty?; d = case input; when Time, Date, DateTime then input; when 'now', 'today' then Time.now; when /\\A\\d+\\z/, Integer then Time.at(input.to_i); when String then (Time.parse(input) rescue input); else input; end; d.respond_to?(:strftime) ? d.strftime(fmt.to_s) : input }.call(#{input}, #{format}))" + "LR.date(#{input}, #{format})" }, 'escape_once' => ->(input, _args, _kwargs, _compiler) { - "__to_s__(#{input}).gsub(/[\"><']|&(?!([a-zA-Z]+|(#\\d+));)/) { |c| {'&'=>'&', '>'=>'>', '<'=>'<', '\"'=>'"', \"'\"=>'''}[c] || c }" + "LR.escape_once(#{input})" }, }.freeze diff --git a/lib/liquid/compile/tags/cycle_compiler.rb b/lib/liquid/compile/tags/cycle_compiler.rb index 17ca8193..86e31374 100644 --- a/lib/liquid/compile/tags/cycle_compiler.rb +++ b/lib/liquid/compile/tags/cycle_compiler.rb @@ -32,7 +32,7 @@ module Liquid code.line "case #{cycle_var} % #{variables.size}" variables.each_with_index do |var, idx| var_expr = ExpressionCompiler.compile(var, compiler) - code.line "when #{idx} then __output__ << __to_s__(#{var_expr})" + code.line "when #{idx} then __output__ << LR.to_s(#{var_expr})" end code.line "end" diff --git a/lib/liquid/compile/tags/for_compiler.rb b/lib/liquid/compile/tags/for_compiler.rb index ebcd2a87..12dc8279 100644 --- a/lib/liquid/compile/tags/for_compiler.rb +++ b/lib/liquid/compile/tags/for_compiler.rb @@ -69,9 +69,9 @@ module Liquid if tag.limit limit_expr = ExpressionCompiler.compile(tag.limit, compiler) - code.line "#{coll_var} = (#{coll_var}.respond_to?(:slice) ? #{coll_var}.slice(__to_integer__(#{from_expr}), __to_integer__(#{limit_expr})) : #{coll_var}) || []" + code.line "#{coll_var} = (#{coll_var}.respond_to?(:slice) ? #{coll_var}.slice(LR.to_integer(#{from_expr}), LR.to_integer(#{limit_expr})) : #{coll_var}) || []" else - code.line "#{coll_var} = (#{coll_var}.respond_to?(:drop) ? #{coll_var}.drop(__to_integer__(#{from_expr})) : #{coll_var}) || []" + code.line "#{coll_var} = (#{coll_var}.respond_to?(:drop) ? #{coll_var}.drop(LR.to_integer(#{from_expr})) : #{coll_var}) || []" end end diff --git a/lib/liquid/compile/tags/tablerow_compiler.rb b/lib/liquid/compile/tags/tablerow_compiler.rb index af1a1713..9ea2839d 100644 --- a/lib/liquid/compile/tags/tablerow_compiler.rb +++ b/lib/liquid/compile/tags/tablerow_compiler.rb @@ -36,13 +36,13 @@ module Liquid offset_expr = ExpressionCompiler.compile(offset, compiler) if limit limit_expr = ExpressionCompiler.compile(limit, compiler) - code.line "#{coll_var} = #{coll_var}.slice(__to_integer__(#{offset_expr}), __to_integer__(#{limit_expr})) || []" + code.line "#{coll_var} = #{coll_var}.slice(LR.to_integer(#{offset_expr}), LR.to_integer(#{limit_expr})) || []" else - code.line "#{coll_var} = #{coll_var}.drop(__to_integer__(#{offset_expr}))" + code.line "#{coll_var} = #{coll_var}.drop(LR.to_integer(#{offset_expr}))" end elsif limit limit_expr = ExpressionCompiler.compile(limit, compiler) - code.line "#{coll_var} = #{coll_var}.first(__to_integer__(#{limit_expr}))" + code.line "#{coll_var} = #{coll_var}.first(LR.to_integer(#{limit_expr}))" end end diff --git a/lib/liquid/compile/variable_compiler.rb b/lib/liquid/compile/variable_compiler.rb index 3ad78efb..7333491b 100644 --- a/lib/liquid/compile/variable_compiler.rb +++ b/lib/liquid/compile/variable_compiler.rb @@ -14,7 +14,7 @@ module Liquid # @param code [CodeGenerator] The code generator def self.compile(variable, compiler, code) value_expr = compile_to_expression(variable, compiler) - code.line "__output__ << __output_value__(#{value_expr})" + code.line "__output__ << LR.output(#{value_expr})" end # Compile a Variable node to a Ruby expression (without output)