Compare commits

...
Author SHA1 Message Date
Michael Go fe2ba1d8da don't create string scanner for liquid tag 2024-11-01 17:31:05 -03:00
Michael Go fa51b8be40 mini opt 2024-11-01 17:30:18 -03:00
Michael Go f77075895a appease rubocop 2024-11-01 17:24:09 -03:00
Michael Go 579b899e24 mini opt 2024-11-01 17:19:50 -03:00
Michael Go 7ac53a3639 use String#split for liquid tag tokenization 2024-11-01 17:11:10 -03:00
Michael Go f49ceed826 mini opt 2024-11-01 17:06:28 -03:00
Michael Go d2b68b81a7 undo mini opt 2024-11-01 17:02:09 -03:00
Michael Go 7d9ffdce03 mini opt 2024-11-01 16:36:25 -03:00
Michael Go 2e4d02f4d1 mini opt 2024-11-01 16:22:01 -03:00
Ian Ker-SeymerandGitHub a0c64e4cbd Update strscan comment 2024-10-31 16:57:52 -04:00
Michael Go b4907f6323 pre-calculate tokens 2024-10-31 15:46:48 -03:00
Michael Go 78ef372878 backward compatible tokenizer 2024-10-30 14:31:55 -03:00
Michael Go 2a829b889e add more quirky lexer parsing unit test 2024-10-30 13:54:01 -03:00
Michael Go 15d117926d more less strict strscan version requirement 2024-10-30 13:54:01 -03:00
Michael Go 936f73dc30 refactor tokenizer to simulate original regex properly 2024-10-30 13:54:01 -03:00
Michael Go fecbc62533 remove unnecessary comparison jump table entries 2024-10-30 13:54:01 -03:00
Michael Go b7b6400985 fix typo 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go be8e329cef more opt 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go d76663f570 more opt 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go f42c6b0608 more opt 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go aa45356133 Avoid method dispatch 2024-10-30 13:54:01 -03:00
Michael Go a406603e9f more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 54975ddac1 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 91e1563e1c more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 0346337c05 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 649b64ffd4 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 48d6a03de6 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 9e0c5f4747 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go e06ac1bc9c more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go c2baa4c270 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go b81d1c8c5d more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go e3c2fd47b2 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go 1b3927ee1d more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go 6ad7508ce2 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go ddb969af8b more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go 128935dcef micro optimization 2024-10-30 13:54:00 -03:00
Michael Go b545e3ae6a lazy tokenizer 2024-10-30 13:54:00 -03:00
Michael Go 59699075a2 refactor lexer unit test 2024-10-30 13:53:57 -03:00
Michael Go 12b5a35e26 fix parsing quirky incomplete expressions 2024-10-30 13:53:34 -03:00
10 changed files with 195 additions and 10 deletions
+2
View File
@@ -28,3 +28,5 @@ group :test do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
end
end
gem "strscan", ">= 3.1"
+4 -1
View File
@@ -73,7 +73,7 @@ end
namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
task :run do
task :lax do
ruby "./performance/benchmark.rb lax"
end
@@ -82,6 +82,9 @@ namespace :benchmark do
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with both lax and strict parsing"
task run: [:lax, :strict]
desc "Run unit benchmarks"
task :unit do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
+3 -2
View File
@@ -92,6 +92,7 @@ module Liquid
SINGLE_COMPARISON_TOKENS = [].tap do |table|
table["<".ord] = COMPARISON_LESS_THAN
table[">".ord] = COMPARISON_GREATER_THAN
table.freeze
end
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
@@ -103,18 +104,17 @@ module Liquid
sub_table["=".ord] = COMPARISION_NOT_EQUAL
sub_table.freeze
end
table.freeze
end
COMPARISON_JUMP_TABLE = [].tap do |table|
table["<".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
sub_table.freeze
end
table[">".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
sub_table.freeze
end
table.freeze
@@ -233,5 +233,6 @@ module Liquid
end
end
# Remove this once we can depend on strscan >= 3.1.1
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
end
+146 -1
View File
@@ -1,7 +1,9 @@
# frozen_string_literal: true
require "strscan"
module Liquid
class Tokenizer
class Tokenizer1
attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@@ -42,4 +44,147 @@ module Liquid
tokens
end
end
class Tokenizer2
attr_reader :line_number, :for_liquid_tag
TAG_END = /%\}/
TAG_OR_VARIABLE_START = /\{[\{\%]/
NEWLINE = /\n/
OPEN_CURLEY = "{".ord
CLOSE_CURLEY = "}".ord
PERCENTAGE = "%".ord
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@source = source
@offset = 0
@tokens = []
tokenize
end
def shift
token = @tokens[@offset]
return unless token
@offset += 1
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
token
end
private
def tokenize
if @for_liquid_tag
@tokens = @source.split("\n")
else
@ss = StringScanner.new(@source)
@tokens << shift_normal until @ss.eos?
end
@ss = nil
@source = nil
end
def shift_normal
token = next_token
return unless token
token
end
def next_token
# possible states: :text, :tag, :variable
byte_a = @ss.peek_byte
if byte_a == OPEN_CURLEY
@ss.scan_byte
byte_b = @ss.peek_byte
if byte_b == PERCENTAGE
@ss.scan_byte
return next_tag_token
elsif byte_b == OPEN_CURLEY
@ss.scan_byte
return next_variable_token
end
@ss.pos -= 1
end
next_text_token
end
def next_text_token
start = @ss.pos
unless @ss.skip_until(TAG_OR_VARIABLE_START)
token = @ss.rest
@ss.terminate
return token
end
pos = @ss.pos -= 2
@source.byteslice(start, pos - start)
end
def next_variable_token
start = @ss.pos - 2
byte_a = byte_b = @ss.scan_byte
while byte_b
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
break unless byte_a
if @ss.eos?
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
end
byte_b = @ss.scan_byte
if byte_a == CLOSE_CURLEY
if byte_b == CLOSE_CURLEY
return @source.byteslice(start, @ss.pos - start)
elsif byte_b != CLOSE_CURLEY
@ss.pos -= 1
return @source.byteslice(start, @ss.pos - start)
end
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
return next_tag_token_with_start(start)
end
byte_a = byte_b
end
"{{"
end
def next_tag_token
start = @ss.pos - 2
if (len = @ss.skip_until(TAG_END))
@source.byteslice(start, len + 2)
else
"{%"
end
end
def next_tag_token_with_start(start)
@ss.skip_until(TAG_END)
@source.byteslice(start, @ss.pos - start)
end
end
# Remove this once we can depend on strscan >= 3.1.1
Tokenizer = StringScanner.instance_methods.include?(:scan_byte) ? Tokenizer2 : Tokenizer1
end
+8 -5
View File
@@ -8,14 +8,17 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 10
x.warmup = 5
x.time = 20
x.warmup = 10
puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts
x.report("parse:") { profiler.compile }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
phase = ENV["PHASE"] || "all"
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
end
+8
View File
@@ -48,6 +48,14 @@ class ThemeRunner
end
end
# `tokenize` will just test the tokenizen portion of liquid without any templates
def tokenize
@tests.each do |test_hash|
tokenizer = Liquid::Tokenizer.new(test_hash[:liquid], true)
while tokenizer.shift; end
end
end
# `run` is called to benchmark rendering and compiling at the same time
def run
each_test do |liquid, layout, assigns, page_template, template_name|
+8
View File
@@ -134,6 +134,14 @@ class ParsingQuirksTest < Minitest::Test
def test_incomplete_expression
with_error_mode(:lax) do
assert_template_result("false", "{{ false - }}")
assert_template_result("false", "{{ false > }}")
assert_template_result("false", "{{ false < }}")
assert_template_result("false", "{{ false = }}")
assert_template_result("false", "{{ false ! }}")
assert_template_result("false", "{{ false 1 }}")
assert_template_result("false", "{{ false a }}")
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
+1
View File
@@ -16,6 +16,7 @@ class RawTagTest < Minitest::Test
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw -%} <")
assert_template_result("{Hello}", "{% raw %}{{% endraw %}Hello{% raw %}}{% endraw %}")
end
def test_open_tag_in_raw
+1 -1
View File
@@ -13,7 +13,7 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
puts "-- #{env_mode.upcase} ERROR MODE"
mode = env_mode.to_sym
end
Liquid::Template.error_mode = mode
Liquid::Environment.default.error_mode = mode
if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C"
+14
View File
@@ -6,6 +6,7 @@ class TokenizerTest < Minitest::Test
def test_tokenize_strings
assert_equal([' '], tokenize(' '))
assert_equal(['hello world'], tokenize('hello world'))
assert_equal(['{}'], tokenize('{}'))
end
def test_tokenize_variables
@@ -30,6 +31,19 @@ class TokenizerTest < Minitest::Test
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
end
def test_incomplete_curly_braces
assert_equal(["{{.}", " "], tokenize('{{.} '))
assert_equal(["{{}", "%}"], tokenize('{{}%}'))
assert_equal(["{{}}", "}"], tokenize('{{}}}'))
end
def test_unmatching_start_and_end
assert_equal(["{{%}"], tokenize('{{%}'))
assert_equal(["{{%%%}}"], tokenize('{{%%%}}'))
assert_equal(["{%", "}}"], tokenize('{%}}'))
assert_equal(["{%%}", "}"], tokenize('{%%}}'))
end
private
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)