This document classifies every Ren'Py syntax construct the parser
encounters, and describes how the parser handles it. The parser is
intentionally conservative: it would rather emit an ImportWarning and
preserve the source verbatim as an UnsupportedStatement than silently
misinterpret the construct.
- Supported — parsed into a typed IR statement and consumed by the importer. Source locations are preserved.
- Partially supported — parsed into a typed IR statement but with known limitations documented below. The importer uses what it can.
- Observed but opaque — recognized as a block boundary (so the
parser does not misinterpret nested content) but the body is kept
verbatim as
UnsupportedStatement. The user sees a warning. - Unsupported with warning — not recognized at all; the line is
recorded as
UnsupportedStatementwith a reason, and anImportWarningis emitted. The parser continues.
| Construct | Form | Classification | Notes |
|---|---|---|---|
label |
label name: / label name(args): |
Supported | Body is recursively parsed. |
jump |
jump target |
Supported | Edge of kind flow, transfer jump. |
jump ... if |
jump target if cond |
Partially supported | Non-standard Ren'Py syntax; tolerated. Condition is preserved on the edge. |
call |
call target / call target(args) |
Supported | Edge of kind flow, transfer call. |
call ... if |
call target if cond |
Partially supported | Non-standard; tolerated. |
return |
return |
Supported | In a sandbox event, becomes a hidden return edge to the inferred hub. |
menu |
menu: / menu name: / menu("arg"): |
Supported | Each choice becomes an edge of kind flow, transfer choice, with choiceText. |
menu choice |
"text": / 'text': / "text" if cond: |
Supported | Condition preserved on the edge. |
if / elif / else |
if cond: / elif cond: / else: |
Supported | Branches parsed recursively; conditions captured as ConditionExpression. |
default |
default var = value |
Supported | Creates a GameVariable with defaultValue. Type inferred from literal. |
define Character |
define id = Character("Name", color="...") |
Supported | Creates a Character. Only the name and color keyword args are extracted; other kwargs are ignored. |
define (non-Character) |
define X = 5 |
Observed but opaque | Treated as unsupported; the right-hand side is not interpreted. |
define gui.* / define config.* |
define gui.text_size = 28 |
Observed but opaque | Proven non-story configuration; filtered out of normal diagnostics and reported as one informational count. |
$ assignment |
$ var op value (op ∈ =, +=, -=, *=, /=) |
Supported | Recorded as VariableMutation on the enclosing node. |
$ (other expression) |
$ foo(bar) |
Unsupported with warning | Recorded as unsupported; importer ignores. |
python: block |
python: followed by indented block |
Observed but opaque | The block is captured as a single UnsupportedStatement; its body is not interpreted. Variables referenced inside are NOT indexed. |
init python: block |
init python: / init -1 python: |
Observed but opaque | Same as python:. |
screen block |
screen name(args): |
Observed but opaque | The entire screen body is captured as one UnsupportedStatement. |
transform block |
transform name: |
Observed but opaque | Same. |
image statement |
image name = ... / image name: |
Unsupported with warning | Line recorded as unsupported. |
layeredimage block |
layeredimage name: |
Observed but opaque | Same. |
scene |
scene bg with fade |
Supported (visual) | Recognized as a VisualStatement. The image expression maps to a BackgroundBlock for imported scene documents. Note: the architectural scene node kind is inferred separately from label-name heuristics. |
show / hide |
show sprite at position |
Supported (visual) | Recognized as a VisualStatement; no scene block is produced. |
with |
with fade |
Supported (visual) | Recognized as a VisualStatement; no scene block. |
play / queue / stop |
play music "track.mp3" |
Supported (visual) | play music/stop music/play sound map to MusicBlock/SoundBlock; other channels are ignored. |
voice |
voice "audio/line.ogg" |
Supported (visual) | Recognized as a VisualStatement; no scene block. |
pause |
pause / pause 2.0 |
Supported (visual) | Recognized as a VisualStatement; no scene block. |
window / centered / vcentered / nvl |
centered "text" |
Supported (visual) | centered "text" / vcentered "text" map to NarrationBlock; the rest are ignored. |
| narrator dialogue | "text" |
Supported | Recorded as DialogueStatement with no character. Does not produce a graph node; only used for character co-occurrence. |
| character dialogue | id "text" |
Supported | Recorded as DialogueStatement with character = id. Links the character to the enclosing node. |
| Custom user-defined statement | mystatement arg1 arg2 |
Unsupported with warning | Line recorded as unsupported with reason = "mystatement". |
| Comment | # ... |
Supported (stripped) | Inline # is stripped from line text; full-line comments are skipped. |
| Blank line | (empty) | Supported (skipped) | No-op. |
The parser uses indentation to determine block boundaries, matching Ren'Py's own behavior. Tabs are expanded to the next tab stop (width 4) for indentation calculation; mixing tabs and spaces inside the same block is not supported and produces a warning.
A line ending with : is treated as a block header. The parser then
recurses into the indented body.
Double-quoted ("...") and single-quoted ('...') string literals are
supported in:
menuchoice text (includingif condforms);- character dialogue (narrator and character-prefixed);
Character("Name")first argument;color="..."keyword argument.
Escaped quotes inside strings (\") are tolerated. The parser also strips
the generator's hidden {#sa_<hex>} choice disambiguation tags on
re-import (both 8-hex and 16-hex suffixes) so repeated identical choices
re-import cleanly. Triple-quoted strings and f-strings are not supported.
The condition parser (condition-parser.ts) extracts:
- the raw condition text (preserved verbatim);
- the set of variable names referenced (used to populate
variableReadsand to drive variable focus mode).
It does not evaluate conditions. It tokenizes Python identifiers,
skips string literals, and excludes Python keywords (True, False,
None, and, or, not, in, is, for, while, return, ...).
Anything else inside a condition (function calls, attribute access,
list/dict indexing, comprehensions) is preserved as raw text and the
variable-name extraction is best-effort.
- It does not resolve Ren'Py auto-callables or screen actions.
- It does not interpret
python:blocks; variables mutated insidepython:blocks are NOT reflected in the variable index. This is a known limitation — users who do significant state mutation insidepython:blocks will see an incomplete variable influence view. - It does not follow
call screenormenuwith a screen argument. - It does not interpret
transformorimagedefinitions. - It does not parse string interpolation
[variable]inside dialogue; such references are not added tovariableReads. This is a known limitation. - It never executes Python.
The shipped fixtures in src/lib/fixtures/index.ts exercise the parser:
- linear-vn — covers
label,jump,menu,if/elif/else,default,define Character,$ assignment, character dialogue, narrator dialogue, andreturn. - sandbox-vn — covers
call/returncycles, gated events withifconditions oncall, hub labels thatjumpto themselves, and multi-file projects. - malformed-vn — covers missing colons, duplicate labels, empty
default = 0,python:andscreen:blocks, and a brokendefinestatement. The parser must survive all of these and produce warnings instead of throwing.
- Add a new variant to
ParsedStatementinparser-types.ts. - Add a regex and a branch in
block-parser.tsparseStatement(). - Decide whether the importer needs to consume the new statement. If
yes, extend
build-game-model.ts(orextract-scene-documents.ts). If no, leave it asUnsupportedStatement. - Add a fixture under
src/lib/fixtures/index.tsthat exercises the new construct. - Update this matrix.
- Run
bun run typecheckandbun run testto verify nothing broke.
The parser is tested by behavior, not by IR shape: fixtures are parsed,
and the resulting GameProject/scene documents are asserted to contain
the expected nodes, edges, variable references, and blocks. The
intermediate ParsedStatement IR is not asserted against in tests — it
is an implementation detail that may change as long as the importer still
produces the same Game Model.