Weavly Compiler
Compiler for the Weavly dialogue scripting language. Parses .wvl files and compiles them to JSON for the Weavly Godot runtime.
Language documentation: https://weavly-lang.github.io/weavly-docs/
Install
Install uv, then:
uv tool install weavly
uv installs a suitable Python if needed and puts weavly on your PATH (run uv tool update-shell if it isn't). Upgrade with uv tool upgrade weavly.
With Python 3.11+ already installed, pipx install weavly works too.
Usage
weavly init my-project # creates my-project/src/nodes.wvl
cd my-project
weavly build # compiles src/**/*.wvl into build/
weavly build --pretty # same, with indented JSON
weavly --version # installed compiler version
weavly init without a name sets up src/ in the current directory.
The build writes:
build/<path>.wvl.jsonfor each source file, containing its nodes and asourcefield with the path relative tosrc/(for example"chapter1/intro.wvl"). Nodes, statements, match and random cases and option items carry the 1-basedlinethey start on, so runtime errors can point back to the.wvlsource.build/env.jsonwith every@envvariable declaration in the project indeclarations, and the names of all pools and slots inpoolsandslots
Commands take comma-separated expressions as arguments and are written with them in args, for the game to evaluate when the command runs:
@play_sound "door", $volume * 0.5
{"type": "command", "line": 1, "id": "play_sound", "args": ["door", {"op": "*", "left": {"variable": "volume"}, "right": 0.5}]}
Arguments are checked like any other expression. Command names aren't declared, so the build doesn't check them or how many arguments they get.
Line, character line, option, hint and continue text can hold any expression inside {}:
The room costs {$base_price * $markup} gold.
@option "Pay {round($price)} gold"
text is written as a list of plain strings and expressions, for the game to evaluate each expression and join the segments. It's a list even without expressions, and never holds empty strings:
{"type": "narration", "line": 1, "text": ["The room costs ", {"op": "*", "left": {"variable": "base_price"}, "right": {"variable": "markup"}}, " gold."]}
Write \{ for a literal brace. A } outside an expression is plain text. String literals inside {} in quoted text escape their quotes like any other quote in it: @option "Greet {$name == \"Bob\"}".
Variables defined outside .wvl, as Godot resources or by game code, are declared with extern and a type, without a default, min or max. They're written to env.json with "extern": true and no value:
@env
score: number = 0
extern reputation: number
@endenv
Storylets are nodes the game picks from a pool instead of a script naming them. Pools and slots are declared in @env, without a default:
@env
cave_outcome: pool
treasure: slot
@endenv
They share names with variables, so a name can be declared only once in the project, but a node id may match one. A node joins pools with a @meta block of key: value entries, right after its @node line:
@node cave_treasure
@meta
pool: cave_outcome
slot: treasure
when: $luck > 5
priority: 1
weight: 2
once: true
@endmeta
You squeeze through the gap...
@endnode
pool: comma-separated pool names, at least one.slot: comma-separated slot names. Nodes sharing a slot exclude each other when the game lists a pool.when: a flag expression, the node is eligible only while it's true.priorityandweight: number expressions. The game defaults them to 0 and 1.once:trueaddsnot visited(<this node>)towhen. It isn't written to the output.
The node gets a meta object with the entries that were written, each with its line and value:
{"id": "cave_treasure", "line": 1, "meta": {
"pool": {"line": 3, "value": ["cave_outcome"]},
"slot": {"line": 4, "value": ["treasure"]},
"when": {"line": 5, "value": {"op": "and", "left": {"op": ">", "left": {"variable": "luck"}, "right": 5.0}, "right": {"op": "not", "expression": {"call": "visited", "node": "cave_treasure"}}}},
"priority": {"line": 6, "value": 1.0},
"weight": {"line": 7, "value": 2.0}
}, "body": [...]}
A when that only comes from once carries the once line.
skip_count(<node>) is how often the node was eligible when the game listed or drew from one of its pools, but wasn't taken. The game resets it when the node is taken, so a storylet that keeps being passed over can raise its own chances:
@meta
pool: cave_outcome
weight: 1 + skip_count()
@endmeta
visited(), visit_count() and skip_count() without an argument mean the node they're written in. The build writes that node's id, as if it had been written out.
@draw plays one storylet from one or more comma-separated pools, as a statement or an inline action:
@node cave_enter
You search the cave.
@draw cave_outcome
You find nothing of interest.
@endnode
{"type": "draw", "line": 3, "pools": ["cave_outcome"]}
pools is always a list, in the written order. Every pool must be declared, but it can still be without members. The game combines the members of all given pools, counting a node that's in several of them once, and picks the eligible node with the highest priority, with weight deciding between equal priorities. It jumps there like @goto. If no node is eligible, execution continues with the next statement, which is where a fallback goes.
Every variable a script uses must be declared, in expressions, as the target of @set, @increase, @decrease, @setflag and @clearflag, as a character line's $name, and in expressions inside {} in text. The build also checks types:
+ - * /, unary-and random weights need numbers;and,orandnotneed flags.- Comparisons need both sides of the same type.
- Conditions (
@if,@elif,@when, option, hint and case conditions) must be flags. @setmust match the variable's type,@increaseand@decreaseneed a number variable,@setflagand@clearflaga flag variable, and a character line's$namea string variable.visited()is a flag,visit_count(),skip_count()and the other built-in functions are numbers, and built-in function arguments are numbers.- Expressions inside
{}in text can be of any type. whenmust be a flag,priorityandweightnumbers, andoncetrueorfalse. Pools and slots can't be used as$name.
Syntax errors, duplicate declarations, number declarations whose min, max or default don't fit together, duplicate node ids, unknown functions, function calls with the wrong number of arguments, @goto, visited(), visit_count() and skip_count() targets with no matching node, undeclared variables, pools and slots, unknown or duplicate @meta keys, and type errors fail the build with exit code 1. A failed build leaves the previous build/ untouched.
Development
git clone https://github.com/weavly-lang/weavly-compiler.git
cd weavly-compiler
uv sync --group dev
See CONTRIBUTING.md for the workflow and release steps.
License
Release files for weavly 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| weavly-0.4.0.tar.gz | 33.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| weavly-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 57.0 kB
Release files / weavly-0.4.0.tar.gz
| Download URL | weavly-0.4.0.tar.gz |
|---|---|
| Size | 33.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f31dd848201de8583f4ba18b1ba9e8af80208833dd2d4273a7f95b233a2b8b0e
|
|
BLAKE2b-256 checksum How to use checksums |
12ef5a703e98cff58d148dfbd591ba7e4f28b8a12e4c44ccdfa0a061c96c7a8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / weavly-0.4.0-py3-none-any.whl
| Download URL | weavly-0.4.0-py3-none-any.whl |
|---|---|
| Size | 24.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fd67ae863a8b669e5bebc79db53f9a4a3559de9b86151bb6d025791e7d719dc2
|
|
BLAKE2b-256 checksum How to use checksums |
f382559a7e31d03c39a7fe72201cee69d4503594bdd421d2944ce69262a6cb7b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log