Skip to main content

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.json for each source file, containing its nodes and a source field with the path relative to src/ (for example "chapter1/intro.wvl"). Nodes, statements, match and random cases and option items carry the 1-based line they start on, so runtime errors can point back to the .wvl source.
  • build/env.json with every @env variable declaration in the project in declarations, and the names of all pools and slots in pools and slots

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.
  • priority and weight: number expressions. The game defaults them to 0 and 1.
  • once: true adds not visited(<this node>) to when. 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, or and not need flags.
  • Comparisons need both sides of the same type.
  • Conditions (@if, @elif, @when, option, hint and case conditions) must be flags.
  • @set must match the variable's type, @increase and @decrease need a number variable, @setflag and @clearflag a flag variable, and a character line's $name a 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.
  • when must be a flag, priority and weight numbers, and once true or false. 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

MIT

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)

Source distribution for weavly 0.4.0
File Size Uploaded
weavly-0.4.0.tar.gz 33.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for weavly 0.4.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page