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.