Skip to content

Compiler

wybthon.compiler

compiler

The Wybthon compiler: Solid's compile step, done on Python's syntax tree.

Solid's compiler does two things with JSX. It turns static markup into templates, and it turns every dynamic expression in the markup into a getter, so {count() * 2} stays reactive without being wrapped. This module does both for html(t"..."):

@component
def Counter(props: CounterProps):
    count, set_count = create_signal(0)
    return html(t"<p>{count()} doubled is {count() * 2}, step {props.step}</p>")

compiles to (roughly):

return html(
    t"<p>{getter(lambda: count())} doubled is {getter(lambda: count() * 2)}, step {getter(lambda: props.step)}</p>",
    __wybthon_t0__,  # the literal's parse tree and mount function, built at compile time
)

The rule. An interpolation is dynamic when its expression contains a call, an attribute access, or a subscript (not counting the bodies of lambdas inside it). A dynamic interpolation becomes a getter: a hole in a child position, a binding in an attribute, a getter prop on a component tag. Event handlers, refs, spreads, the component in a component tag, and nested html(t"...") markup are never wrapped. Anything that isn't dynamic (a name, a constant, a lambda) is passed as is.

Calls in child position are dispatched when the template is created (see wybthon._compiled.call): a component (Card(title=user().name), Show(...), For(...)) is built with each dynamic argument as a getter, the way a JSX component receives its props; any other callable (badge(user()), str(n())) becomes one reactive expression.

Captured names. A getter runs later, so the names its expression reads from enclosing function scopes are captured by value when the template is created (as keyword-only defaults of the getter's lambda). A template made in a loop sees that iteration's values.

Precompiled templates. Each literal is parsed and validated here, so a malformed template is a SyntaxError with its file and line. Its parse tree and the mount function for each of its elements are written into the module, so html() never parses a template or generates code at run time.

The compiler runs from the import hook that import wybthon installs, from wyb build and wyb dev, from the pytest plugin, and from wyb run.

Functions:

Name Description
install

Compile templates in modules imported from now on (idempotent).

is_candidate

Whether a module's source may contain html(t"...") calls for the compiler.

uninstall

Remove the import hook.

is_dynamic

Whether an expression may read reactive state: it contains a call, attribute access, or subscript.

transform

Compile the html(t"...") calls in a module's syntax tree, in place.

compile_source

Compile a module's source with its templates compiled.

defers

Whether the compiler would defer an interpolation's expression (dev-mode checks).

install

install() -> None

Compile templates in modules imported from now on (idempotent).

import wybthon calls this. The hook goes right before the regular path finder, so finders installed earlier (pytest's assertion rewriting, for one) keep handling the modules they claim.

is_candidate

is_candidate(source: bytes) -> bool

Whether a module's source may contain html(t"...") calls for the compiler.

uninstall

uninstall() -> None

Remove the import hook.

is_dynamic

is_dynamic(node: AST) -> bool

Whether an expression may read reactive state: it contains a call, attribute access, or subscript.

The bodies of lambdas inside the expression don't count; they run whenever their caller decides.

transform

transform(tree: Module, filename: str = '<unknown>', source: str = '') -> Module

Compile the html(t"...") calls in a module's syntax tree, in place.

Returns the tree. A module that doesn't import Wybthon's html is returned unchanged. Raises SyntaxError for a malformed template.

compile_source

compile_source(source: str | bytes, filename: str) -> CodeType

Compile a module's source with its templates compiled.

defers cached

defers(expression: str, position: str) -> bool

Whether the compiler would defer an interpolation's expression (dev-mode checks).

html() asks this about templates that reach it uncompiled: their dynamic expressions have already been evaluated once.

What's in this module

compiler is Wybthon's compile step, the counterpart of Solid's JSX compiler, done on Python's syntax tree. It rewrites every html(t"...") call whose argument is a t-string literal: each dynamic interpolation (one containing a call, an attribute access, or a subscript) becomes a getter, a call in a child position is dispatched to a component or made a reactive hole, and the literal's parse tree and mount functions are emitted as ordinary module code. Nothing else in the module changes. See Concepts: Compiler for the rules and how to inspect the output.

You rarely call it directly. It runs in wyb build and wyb dev, in the import hook that import wybthon installs, in the pytest plugin, and in wyb run script.py.

Name Description
install Install the import hook (idempotent); import wybthon calls it.
uninstall Remove the import hook.
compile_source compile_source(source, filename): a module's code object with its templates compiled, ready for exec.
transform transform(tree, filename="<unknown>", source=""): rewrite a module's ast in place and return it; raises SyntaxError for a malformed template.
is_dynamic Whether an expression node contains a call, attribute access, or subscript (outside the lambdas inside it).
is_candidate Whether a module's source bytes might hold html(t"...") calls: it mentions wybthon and html and contains a t-string.
defers Whether the compiler would defer an interpolation's source text in a given position; html() uses it for the dev-mode check of uncompiled templates.
COMPILER_TAG The optimization tag of cached compiled bytecode (*.opt-wyb1.pyc); it changes when the compiler's output does.
from wybthon.compiler import compile_source

source = """
from wybthon import create_signal, html

count, set_count = create_signal(2)
view = html(t"<p>{count() * 2}</p>")
"""

namespace = {}
exec(compile_source(source, "generated.py"), namespace)  # {count() * 2} is a reactive hole
  • html is resolved through the module's imports (from wybthon import html, from wybthon.templates import html, an alias of either, or wybthon.html), so a module that defines its own html is left alone.
  • A malformed template is a SyntaxError with its file and line when the module is compiled, not a TemplateError at run time.
  • The generated names (__wybthon__, __wybthon_t0__, __wybthon_m0__) are dunder names, which Python never mangles inside classes.

See also