class

Crinja

Inherits Crinja::Resolver < Reference < Object

=== 5+6. Loop-invariant fragment caching & iteration scope reuse =========

Both patches key off the same static property of a for-loop, which we call a closed body: every identifier the body (and the optional if condition) reads is bound by the loop itself (the item variables or loop), the body contains no nested tags other than if/elif/else with equally closed conditions, and no calls/filters/tests. Such a body can neither observe per-page state nor capture the iteration context (no {% block scoped %}, no macro definitions, no callables), so:

  1. Fragment cache — the loop's entire output is a pure function of (loop AST node, collection contents, autoescape). Listing templates like {% for p in site.pages %}<li>{{ p.title }}</li>{% endfor %} re-render byte-identical output once per page; on the harsh-5000 corpus that loop alone accounts for most of the render phase. Cache the flattened output per (tag node, collection object, autoescape) on the environment. Correctness leans on two facts: hwaro converts site/section collections to Crinja arrays once per build (stable object identity ⇒ stable contents — the site is frozen during render), and each render worker owns its env exclusively (no concurrent mutation; the env — and so the cache — lives for a single render pass). Entries pin their tag node and collection array, so an object_id can never be recycled into a false hit. The cache is size-capped: loops over per-page collections ({% for t in p.tags %}) insert entries that never hit again, so past the cap we render normally instead of inserting.

  2. Scope reuse — upstream run_loop allocates a fresh Context (scope Hash, macros Hash, four CallStacks) plus a bindings Hash per iteration. For a closed body nothing can retain the iteration context beyond its iteration, so one Context reused across iterations (item vars simply overwritten by unpack) is observationally identical. Non-closed bodies keep the allocating path.

Byte-compat verified like patches 1-4: diff -r of full output trees on harsh-5000, public-5000 and docs, plus a loop-shape torture site.

Constants

SLICE_MEMO_MAX = 256

Slices of one list, memoized per environment: the same pages[:10] on every page returns the SAME array, so a loop over it hits the for-loop fragment cache (keyed by collection identity, crinja_render_perf.cr) instead of filling it with one dead entry per page. Sound for the same reason that cache is: a list's contents never change under a fixed identity during a render pass (templates cannot mutate lists, and hwaro builds its collections once per build). Entries pin their source list so its object_id cannot be recycled into a false hit; past the cap, slices are simply built fresh.

Class methods

contiguous_slice(size : Int, start : Int64 | Nil, stop : Int64 | Nil) : ::Tuple(Int64, Int64)

First index and length of a step-1 slice.

Source
each_slice_index(size : Int, start : Int64 | Nil, stop : Int64 | Nil, step : Int64, &) : Nil

Python's slice index walk (PySlice_AdjustIndices): yields each selected index of a sequence of size items, in order.

Source
slice_string(string : String, start : Int64 | Nil, stop : Int64 | Nil, step : Int64) : String
Source

Instance methods

__for_fragment_cache

Fragment cache: {tag node id, collection array id, autoescape} → flattened output. Lazy so envs that never render a cacheable loop pay nothing.

Source
__slice_memo
Source
__slice_of(source : Array(Value), start : Int64 | Nil, stop : Int64 | Nil, step : Int64) : Array(Value)
Source
hwaro_include_chain

Templates entered through {% include %}, innermost last.

This lives on the environment because that is the object that stays identical across a nested include: Tag::Include renders through env.get_template(...), and TemplateCache::InMemory keys its entries by env, so the template rendered inside an include always carries this very instance. hwaro hands every render worker its own environment (Render#create_fresh_crinja_env), so this array is never shared between fibers.

Source
resolve(name : String) : Value

Resolves a variable in the current context.

Source

Nested types