Major optimizations to reduce allocations and improve execution speed:
1. For loops: Replace catch/throw with while + break flag
- Uses while loop with index instead of .each with catch/throw
- Break implemented with flag variable, continue with next
- Result: 18% fewer allocations, 85% faster for simple loops
2. Forloop property inlining
- Inline forloop.index as (__idx__ + 1), forloop.first as (__idx__ == 0), etc.
- Completely eliminates forloop hash allocation when all properties inlinable
- Result: Loop with forloop went from +46% MORE to -16% FEWER allocations
3. LR.to_array helper with EMPTY_ARRAY constant
- Centralized array conversion with frozen empty array for nil
- Avoids allocations for empty collections
4. Inline LR.truthy? calls
- Replace LR.truthy?(x) with (x != nil && x != false)
- Eliminates method call overhead in conditions
5. Keep Time methods available in sandbox for date filter
Overall results:
- Allocations: 3.5% MORE -> 24% FEWER (27% improvement)
- Time: 64% faster -> 89% faster (25% improvement)
Also adds:
- compile_profiler.rb for measuring allocations/performance
- compile_acceptance_test.rb for output equivalence testing
- OPTIMIZATION.md documenting optimization status
Tags can now implement to_ruby(code, compiler) to provide their own
optimized Ruby code generation. This allows:
1. Third-party tags to participate in compilation
2. Shopify-specific tags to be lowered to Ruby
3. Better performance for frequently-used custom tags
Priority order for tag compilation:
1. tag.to_ruby(code, compiler) - Custom implementation
2. Built-in compiler (IfCompiler, ForCompiler, etc.)
3. Yield to caller as external tag
Also updated PROJECT.md with:
- External calls yielding documentation
- to_ruby convention documentation
Instead of trying to handle external tags/filters inside the sandbox,
yield back to the caller with [:tag, ...] or [:filter, ...] args.
This cleanly separates concerns:
- Sandbox handles compiled template logic
- Caller handles external calls with full Ruby access
API:
compiled.render(assigns) do |call_type, *args|
case call_type
when :tag
tag_var, tag_assigns = args
# Handle with full Liquid context
when :filter
filter_name, input, filter_args = args
# Handle with custom filter handler
end
end
If no block is given, a default handler is used that:
- Renders external tags using Liquid::Context
- Calls filter methods via filter_handler
Also:
- Keep public_send in sandbox (safe, only calls public methods)
- Load date/time libs into sandbox for date filter support
- Preserve Date, DateTime, Time constants after lock
- compiled_template.rb: Use Liquid::Box for secure execution on Ruby 4.0+
- Creates box, loads runtime, locks, then evals template code
- Provides render() method and secure? check
- Falls back to insecure eval with warning on Ruby < 4.0
- ruby_compiler.rb: Remove inline helper generation
- Helpers now provided by pre-loaded LR module
- Generated code is much smaller (just control flow + LR calls)
- compile.rb: Update documentation for new security model
- template.rb: Update compile_to_ruby docs
The LR module provides all helper methods for compiled templates.
It is loaded into the sandbox BEFORE lock!, so helpers are defined
once and shared by all templates.
Helpers include:
- Type conversion: to_s, to_number, to_integer
- Output: output (handles nil, arrays, BigDecimal formatting)
- Lookup: lookup (hash/array access, Drop context support)
- Encoding: escape_html, url_encode/decode, base64_encode/decode
- Filters: truncate, truncatewords, slice, date, default, etc.
Method references to CGI, Base64, BigDecimal are captured at load
time, enabling safe use of these libraries within the sandbox.
Design principle: Maximize work in pre-loaded runtime, minimize
generated template code.
Introduces Liquid::Box which wraps Ruby 4.0's Ruby::Box for secure
template execution. On Ruby < 4.0, provides a polyfill with security
warnings.
Key features:
- Detects Ruby::Box availability at load time
- Loads safe libraries (CGI, Base64, BigDecimal) into sandbox
- Neuters dangerous methods (file IO, process control, eval, etc.)
- Preserves user constants defined before lock!
- Provides setup_gem_load_paths! to enable gem requires in box
Security model: It is safe to expose side-effect-free, non-IO methods
that don't leak objects with dangerous methods. The sandbox blocks
capabilities, not data.