From 52040335885f3f412286c2553b4a2750a1fd3ea4 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Wed, 11 Mar 2026 09:19:27 -0400 Subject: [PATCH] Cursor: add skip_id, expect_id, skip_fragment for zero-alloc scanning --- lib/liquid/cursor.rb | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/lib/liquid/cursor.rb b/lib/liquid/cursor.rb index 753c7a9d..07b34e8e 100644 --- a/lib/liquid/cursor.rb +++ b/lib/liquid/cursor.rb @@ -53,6 +53,11 @@ module Liquid @ss.string = source end + # Extract a slice from the source (deferred allocation) + def slice(start, len) + @source.byteslice(start, len) + end + # ── Whitespace ────────────────────────────────────────────────── # Skip spaces/tabs/newlines/cr, return count of newlines skipped def skip_ws @@ -80,6 +85,39 @@ module Liquid end # ── Identifiers ───────────────────────────────────────────────── + # Skip an identifier without allocating a string. Returns length skipped, or 0. + def skip_id + start = @ss.pos + b = @ss.peek_byte + return 0 unless b && ((b >= 97 && b <= 122) || (b >= 65 && b <= 90) || b == USCORE) + @ss.scan_byte + while (b = @ss.peek_byte) + break unless (b >= 97 && b <= 122) || (b >= 65 && b <= 90) || + (b >= 48 && b <= 57) || b == USCORE || b == DASH + @ss.scan_byte + end + @ss.scan_byte if @ss.peek_byte == QMARK + @ss.pos - start + end + + # Check if next id matches expected string, consume if so. No allocation. + def expect_id(expected) + start = @ss.pos + len = skip_id + if len == expected.bytesize + match = true + len.times do |i| + if @source.getbyte(start + i) != expected.getbyte(i) + match = false + break + end + end + return true if match + end + @ss.pos = start + false + end + # Scan a single identifier: [a-zA-Z_][\w-]*\?? # Returns the string or nil if not at an identifier def scan_id @@ -188,6 +226,25 @@ module Liquid @source.byteslice(start, @ss.pos - start) end + # Skip a fragment without allocating. Returns length skipped, or 0. + def skip_fragment + b = @ss.peek_byte + return 0 unless b + start = @ss.pos + if b == QUOTE_S || b == QUOTE_D + quote = b + @ss.scan_byte + @ss.scan_byte while (b = @ss.peek_byte) && b != quote + @ss.scan_byte if @ss.peek_byte == quote + else + while (b = @ss.peek_byte) + break if b == SPACE || b == TAB || b == NL || b == CR || b == COMMA || b == PIPE + @ss.scan_byte + end + end + @ss.pos - start + end + # Scan a "QuotedFragment" — a quoted string or non-whitespace/comma/pipe run def scan_fragment b = @ss.peek_byte