Skip to content

Compile errors

situ fails closed: everything outside the compiled dialect, the seam, or the composition envelope is a CompileError raised at compile time, so problems surface as visible errors while you develop. In dev the error appears on your framework's debug page (Litestar's or Flask's); in the lint loop, tests and situ check catch it. The compiler carries 111 distinct rejection sites; the inline messages are maintained as part of the API. This page catalogs the ones you'll actually meet, with their fixes.

The seam

Message (abridged) Fix
server-sited 'x' cannot be read on the client; DB-backed state stays on the server Render it server-side (Jinja / context()), ship named row fields explicitly, or push a value with signal() from a command
cannot assign server-sited signal 'x' on the client Mutate through a command (await facade...)
cannot assign url-sited signal 'x' on the client Url state is set by navigation: use a link
refusing to compile a loop over server-sited 'x': DB iteration stays on the server Loop in context() or the facade; the template's Jinja {% for %} renders the result
await in a client position (an @event, a client body) Only a handler body may await, which makes it a server command

The dialect

Message (abridged) Fix
cannot compile expression: <NodeType> / statement not in the compiled dialect: <NodeType> Restructure into the dialect, or move the logic server-side
cannot compile this comparison: the type of \x` is not known here` Give the operand a type; see below
unknown name / unsupported call / unsupported method Only signals, row vars, and the whitelisted functions exist client-side
f-string conversion (!r) or format spec (:.2f) rejected Format server-side, or simplify
membership can't chain with comparisons Split the condition
dict keys must be string constants

An operand whose type is not known

cannot compile this comparison: the type of `id` is not known here, and situ will not decide it
with a runtime coercion. Give it a type — annotate the handler parameter (`def go(id: int)`),
declare the signal's element type (`Local[list[Row]]`), or declare the row's record with
`data-row="<Record>"` — and the comparison compiles to the lowering its types imply.

Situ compiles == / != / in from the operand types: str == str is a plain compare, int == int normalizes through Number(), list == list is structural. When it cannot establish a type, there is no rule to apply, so it refuses.

It used to guess. A runtime helper coerced both sides through Number() and compared, which reported 3 == [3] as true and made a non-numeric value (a status string, a filter id) never compare equal. That helper is gone as of 0.4.0.

Most often the operand is a handler parameter missing an annotation, and situ already infers those from their call sites: go(t.id) with t.id: int types go's parameter for you. You see this error when inference could not settle it: the call sites disagree, one of them passes something untypable, or the handler is never called.

The three fixes, in the order you will usually reach for them:

# 1. annotate the parameter; always works, and says what you meant
def go(id: int):
    if id == selected:
        ...

# 2. declare the signal's element type, so a row read has one
rows: Local[list[Issue]] = []      # not a bare Local[list]

# 3. declare the row's record, so `t.field` is decoded to its real type
#    <li :each="t in rows" data-row="Issue">
class Issue(TypedDict):
    id: int
    title: str

Templates

Message (abridged) Fix
:each needs 'var in iterable' The binder value is row in signal
a binder/event on the :each element itself Put :each on a wrapper element
a row event other than @click/@submit Restructure; rows support click/submit
:bind on a non-Local signal Two-way binding is client state by definition
reactive element outside <header> / data-region (silently inert at runtime) Respect the two runtime rules
context()'s TypedDict omits a name the template reads (opt-in) Add the key to the TypedDict, or fix the template; typing context()'s return is what turns this check on
a {% for t in coll %} reads t.field absent from coll's typed row model (opt-in) Fix the field name (a typo), or add it to the model; situ types coll's rows from context()'s TypedDict (coll: list[Row])
obj.field read, field absent from the type given to obj (opt-in) Fix the field name, or add it to obj's model; the type comes from context()'s TypedDict (obj: Message)
a {% for … in coll %} loops coll, but its TypedDict type is not a collection (opt-in) Type it as a collection; a forgotten list[...] (coll: Row should be coll: list[Row])

Composition

Message (abridged) Fix
unknown component <Tag> Register it (components= / Context.from_dir), and run situ check in your lint loop so this fails fast
missing / extra / duplicate / unused Prop or Emit binding Match the child's declared contract exactly
Inject with no ancestor Provide Provide it on an ancestor, or thread a Prop
content given to a child that declares no <slot> Add a <slot> to the child (content would otherwise vanish)
signal declared by two components / minted-name collision Rename; flat-island names are per-tree
per-instance state inside a :each via an intermediate Fenced: give the state to the row (top-level :each) or lift it
bare prop on a nested host (use the colon form :prop=) Only :/@ bindings substitute through composition levels
component placement cycle: A > B > A / self-placement Composition is a finite tree; data-driven recursion uses :each + :tree
a top-level component's <style> Only child components are CSS-scoped; move page styles to the page

declui

Message (abridged) Fix
unmapped field type Add a widget= or change the type: declui never degrades to a text box
predicate: identifier / call / node outside the grammar Stay in the predicate vocabulary
Decimal relational compare in a predicate String-valued for precision; compare equality, or model as int/float
@action gate referencing a date field, or calling a function The server-row grammar excludes them
model field named value / editing; action named reset Reserved names
a local variable shadowing a field in a sync action Rename the local; it would silently write the signal
server mount passed screens= / rows= / choices=; service_type without context The fail-closed mount rules

The philosophy

A bounded compiler earns its keep by where it stops. If an error message doesn't point at the fix, treat that as a bug and report the message text.