Kida's compilation and rendering pipeline.
Overview
Template Source → Lexer → Parser → Kida AST → Compiler → Python AST → exec()
Pipeline Stages
1. Lexer
Tokenizes template source into a stream of tokens.
from kida.lexer import Lexer, LexerConfig
lexer = Lexer("Hello, {{ name }}!", LexerConfig())
tokens = list(lexer.tokenize())
# [DATA("Hello, "), VARIABLE_BEGIN, NAME("name"), VARIABLE_END, DATA("!"), EOF]
Token types:
DATA— Raw text contentVARIABLE_BEGIN/VARIABLE_END—{{ ` and ` }}BLOCK_BEGIN/BLOCK_END—{%and%}NAME,STRING,INTEGER,FLOAT— Expression tokens
2. Parser
Builds an immutable Kida AST from the token stream.
from kida.parser import Parser
parser = Parser(tokens, name="template.html", filename="template.html", source=source)
ast = parser.parse()
AST nodes:
Template— Root containerData— Static textOutput—{{ expr }}If,For,Match— Control flowBlock,Extends— Inheritance
3. Compiler
Transforms Kida AST to Python AST directly.
from kida.compiler import Compiler
compiler = Compiler(env)
code = compiler.compile(ast, name="template.html")
# Returns compiled code object
Key difference from Jinja2: Kida generatesast.Moduleobjects directly, not Python source strings. This enables:
- Structured code manipulation
- Compile-time optimization
- Precise error source mapping
4. Template
Wraps the compiled code with the render interface.
# Templates are created internally by Environment
# Direct construction (for reference):
template = Template(env, code, name="template.html", filename="template.html")
html = template.render(name="World")
Rendering
Kida uses the StringBuilder pattern for O(n) rendering:
# Generated render function (simplified)
def _render(context):
_out = []
_out.append("Hello, ")
_out.append(_escape(context["name"]))
_out.append("!")
return "".join(_out)
Benefits:
- O(n) string construction (vs O(n²) concatenation)
- Lower memory churn than generators
- Faster than yield-based approaches (see benchmarks)
Caching Architecture
Three cache layers:
Bytecode Cache (Disk)
Persists compiled bytecode viamarshal:
from pathlib import Path
from kida.bytecode_cache import BytecodeCache
cache = BytecodeCache(Path(".kida-cache"))
cache.set(name, source_hash, code)
cached = cache.get(name, source_hash)
Benefits: Significant cold-start improvement for serverless deployments.
Template Cache (Memory)
LRU cache of compiled Template objects:
env = Environment(cache_size=400)
info = env.cache_info()["template"]
# {'size': 50, 'max_size': 400, 'hits': 1000, 'misses': 50}
Fragment Cache (Memory)
TTL-based cache for{% cache %}blocks:
env = Environment(
fragment_cache_size=1000,
fragment_ttl=300.0, # 5 minutes
)
Design Principles
1. AST-Native
No string manipulation or regex. The entire pipeline operates on structured AST objects.
2. Free-Threading Ready
- Compilation is idempotent
- Rendering uses only local state
- Caches use atomic operations
- No shared mutable state
3. Zero Dependencies
Pure Python, no runtime dependencies. Includes nativeMarkupclass.
See Also
- Performance — Benchmarks
- Thread Safety — Free-threading details
- API Reference — Public interface