weavly 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
weavly-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Weavly contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
weavly-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: weavly
3
+ Version: 0.1.0
4
+ Summary: Compiler for the Weavly dialog scripting language
5
+ Author: Weavly contributors
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/weavly-lang/weavly-compiler
8
+ Project-URL: Documentation, https://weavly-lang.github.io/weavly-docs/
9
+ Project-URL: Issues, https://github.com/weavly-lang/weavly-compiler/issues
10
+ Keywords: weavly,dialog,compiler,godot,interactive-fiction
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Games/Entertainment
20
+ Classifier: Topic :: Software Development :: Compilers
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: lark>=1.3.1
25
+ Requires-Dist: typer>=0.21.0
26
+ Dynamic: license-file
27
+
28
+ # Weavly Compiler
29
+
30
+ Compiler for the Weavly dialog scripting language. Parses `.wvl` files and compiles them to JSON for the Weavly Godot runtime.
31
+
32
+ Language documentation: https://weavly-lang.github.io/weavly-docs/
33
+
34
+ ## Install
35
+
36
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then:
37
+
38
+ ```bash
39
+ uv tool install weavly
40
+ ```
41
+
42
+ 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`.
43
+
44
+ With Python 3.11+ already installed, `pipx install weavly` works too.
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ weavly init my-project # creates my-project/src/nodes.wvl
50
+ cd my-project
51
+ weavly build # compiles src/**/*.wvl into build/
52
+ weavly build --pretty # same, with indented JSON
53
+ weavly --version # installed compiler version
54
+ ```
55
+
56
+ `weavly init` without a name sets up `src/` in the current directory.
57
+
58
+ The build writes:
59
+
60
+ - `build/<path>.wvl.json` for each source file, containing its nodes
61
+ - `build/env.json` with every `@env` declaration in the project
62
+
63
+ Syntax errors, duplicate variable declarations, number declarations whose min, max or default don't fit together, duplicate node ids and `@goto` targets with no matching node fail the build with exit code 1. A failed build leaves the previous `build/` untouched.
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ git clone https://github.com/weavly-lang/weavly-compiler.git
69
+ cd weavly-compiler
70
+ uv sync --group dev
71
+ ```
72
+
73
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow and release steps.
74
+
75
+ ## License
76
+
77
+ [MIT](LICENSE)
weavly-0.1.0/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Weavly Compiler
2
+
3
+ Compiler for the Weavly dialog scripting language. Parses `.wvl` files and compiles them to JSON for the Weavly Godot runtime.
4
+
5
+ Language documentation: https://weavly-lang.github.io/weavly-docs/
6
+
7
+ ## Install
8
+
9
+ Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then:
10
+
11
+ ```bash
12
+ uv tool install weavly
13
+ ```
14
+
15
+ 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`.
16
+
17
+ With Python 3.11+ already installed, `pipx install weavly` works too.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ weavly init my-project # creates my-project/src/nodes.wvl
23
+ cd my-project
24
+ weavly build # compiles src/**/*.wvl into build/
25
+ weavly build --pretty # same, with indented JSON
26
+ weavly --version # installed compiler version
27
+ ```
28
+
29
+ `weavly init` without a name sets up `src/` in the current directory.
30
+
31
+ The build writes:
32
+
33
+ - `build/<path>.wvl.json` for each source file, containing its nodes
34
+ - `build/env.json` with every `@env` declaration in the project
35
+
36
+ Syntax errors, duplicate variable declarations, number declarations whose min, max or default don't fit together, duplicate node ids and `@goto` targets with no matching node fail the build with exit code 1. A failed build leaves the previous `build/` untouched.
37
+
38
+ ## Development
39
+
40
+ ```bash
41
+ git clone https://github.com/weavly-lang/weavly-compiler.git
42
+ cd weavly-compiler
43
+ uv sync --group dev
44
+ ```
45
+
46
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow and release steps.
47
+
48
+ ## License
49
+
50
+ [MIT](LICENSE)
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "weavly"
3
+ version = "0.1.0"
4
+ description = "Compiler for the Weavly dialog scripting language"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Weavly contributors" }]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "lark>=1.3.1",
12
+ "typer>=0.21.0",
13
+ ]
14
+ keywords = ["weavly", "dialog", "compiler", "godot", "interactive-fiction"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Games/Entertainment",
25
+ "Topic :: Software Development :: Compilers",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/weavly-lang/weavly-compiler"
30
+ Documentation = "https://weavly-lang.github.io/weavly-docs/"
31
+ Issues = "https://github.com/weavly-lang/weavly-compiler/issues"
32
+
33
+ [project.scripts]
34
+ weavly = "weavly.cli:app"
35
+
36
+ [build-system]
37
+ requires = ["setuptools>=77"]
38
+ build-backend = "setuptools.build_meta"
39
+
40
+ [tool.setuptools]
41
+ package-dir = {"" = "src"}
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.setuptools.package-data]
47
+ weavly = ["resources/*.lark"]
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ "isort>=7.0.0",
52
+ "pytest>=8.0.0",
53
+ "ruff>=0.14.11",
54
+ ]
55
+
56
+ [tool.isort]
57
+ profile = "black"
58
+ line_length = 88
59
+ known_first_party = ["weavly"]
60
+ sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
weavly-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,82 @@
1
+ from importlib.metadata import version as package_version
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from .parsing import (
7
+ WVL_SOURCE_EXTENSION,
8
+ build_all_files,
9
+ )
10
+ from .reporting import report_error
11
+
12
+ SOURCE_DIR = Path("src")
13
+ BUILD_DIR = Path("build")
14
+
15
+ NODE_INIT_STRING = (
16
+ "@env\n"
17
+ "name: string = \"World\"\n"
18
+ "@endenv\n"
19
+ "\n"
20
+ "@node first_node\n"
21
+ "Hello $name!\n"
22
+ "@endnode\n"
23
+ )
24
+
25
+ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
26
+
27
+
28
+ def _print_version(value: bool) -> None:
29
+ if value:
30
+ typer.echo(f"weavly {package_version('weavly')}")
31
+ raise typer.Exit()
32
+
33
+
34
+ @app.callback()
35
+ def main(
36
+ version: bool = typer.Option(
37
+ False,
38
+ "--version",
39
+ callback=_print_version,
40
+ is_eager=True,
41
+ help="Show the version and exit.",
42
+ ),
43
+ ):
44
+ """Compiler for the Weavly dialog scripting language."""
45
+
46
+
47
+ @app.command()
48
+ def build(
49
+ pretty: bool = typer.Option(
50
+ False, "--pretty", help="Write indented, human-readable JSON."
51
+ ),
52
+ ):
53
+ """Compile src/**/*.wvl into build/."""
54
+ count = build_all_files(SOURCE_DIR, BUILD_DIR, pretty)
55
+ files = "file" if count == 1 else "files"
56
+ typer.echo(f"Built {count} {files} into {BUILD_DIR.as_posix()}/")
57
+
58
+
59
+ @app.command()
60
+ def init(
61
+ name: str = typer.Argument(
62
+ None, help="Directory to create. Defaults to the current directory."
63
+ ),
64
+ ):
65
+ """Create a new Weavly project with a starter src/nodes.wvl."""
66
+ if name:
67
+ base = Path(name)
68
+ if base.exists():
69
+ report_error(f"project '{name}' already exists")
70
+ raise typer.Exit(code=1)
71
+ base.mkdir()
72
+ else:
73
+ base = Path()
74
+
75
+ src: Path = base / "src"
76
+ if src.exists():
77
+ report_error(f"directory '{src.as_posix()}' already exists")
78
+ raise typer.Exit(code=1)
79
+ src.mkdir()
80
+ nodes_file = src / f"nodes{WVL_SOURCE_EXTENSION}"
81
+ nodes_file.write_text(NODE_INIT_STRING, encoding="utf-8")
82
+ typer.echo(f"Created {nodes_file.as_posix()}")
@@ -0,0 +1,7 @@
1
+ from .parser import WVL_BUILD_EXTENSION, WVL_SOURCE_EXTENSION, build_all_files
2
+
3
+ __all__ = [
4
+ "build_all_files",
5
+ "WVL_SOURCE_EXTENSION",
6
+ "WVL_BUILD_EXTENSION",
7
+ ]
@@ -0,0 +1,254 @@
1
+ import json
2
+ import shutil
3
+ from importlib import resources
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from lark import Lark, Token, Transformer, Tree
8
+ from lark.exceptions import UnexpectedInput, VisitError
9
+
10
+ from ..reporting import report_error
11
+ from .syntax_errors import describe_syntax_error, terminal_names
12
+ from .wvl_transformer import InvalidStringError, WvlTransformer
13
+
14
+ PARSER_TYPE = "lalr"
15
+ ENCODING = "utf-8"
16
+ SOURCE_ENCODING = "utf-8-sig"
17
+
18
+ WVL_SOURCE_EXTENSION = ".wvl"
19
+ WVL_BUILD_EXTENSION = ".wvl.json"
20
+ WVL_GRAMMAR_FILE = "wvl-grammar.lark"
21
+ ENV_BUILD_FILE = "env.json"
22
+
23
+ _DECLARATION_RULES = frozenset(
24
+ {"number_declaration", "string_declaration", "flag_declaration"}
25
+ )
26
+ _NODE_RULES = frozenset({"node_start"})
27
+ _GOTO_RULES = frozenset({"goto", "inline_goto"})
28
+
29
+ Location = tuple[Path, int, int]
30
+
31
+
32
+ def build_all_files(src_dir: Path, build_dir: Path, pretty: bool) -> int:
33
+ if not src_dir.is_dir():
34
+ report_error(
35
+ f"no '{src_dir.as_posix()}' directory found. Run 'weavly init' to create "
36
+ "a project or run the build from the project root."
37
+ )
38
+ raise typer.Exit(code=1)
39
+
40
+ grammar = _load_grammar(WVL_GRAMMAR_FILE)
41
+ parser = _build_parser(grammar, PARSER_TYPE)
42
+ names = terminal_names(parser)
43
+ transformer = WvlTransformer()
44
+
45
+ declarations: list[dict] = []
46
+ outputs: list[tuple[Path, dict]] = []
47
+ # name -> location of the first declaration seen with that name.
48
+ declared: dict[str, Location] = {}
49
+ node_ids: dict[str, Location] = {}
50
+ gotos: list[tuple[str, Location]] = []
51
+ validation_errors: list[tuple[Location, str]] = []
52
+ failed = False
53
+
54
+ for file in sorted(src_dir.rglob(f"*{WVL_SOURCE_EXTENSION}")):
55
+ if not file.is_file():
56
+ continue
57
+
58
+ relative_path = file.relative_to(src_dir)
59
+ out_file = relative_path.with_suffix(WVL_BUILD_EXTENSION)
60
+
61
+ try:
62
+ text = file.read_text(encoding=SOURCE_ENCODING)
63
+ except UnicodeDecodeError as e:
64
+ _report_encoding_error(e, file)
65
+ failed = True
66
+ continue
67
+
68
+ try:
69
+ tree = parser.parse(text)
70
+ except UnexpectedInput as e:
71
+ _report_parse_error(e, file, text, names)
72
+ failed = True
73
+ continue
74
+
75
+ try:
76
+ data = transformer.transform(tree)
77
+ except VisitError as e:
78
+ if not isinstance(e.orig_exc, InvalidStringError):
79
+ raise
80
+ _report_string_error(e.orig_exc, file)
81
+ failed = True
82
+ continue
83
+
84
+ # Source locations come from the raw tree; the transformed data has no
85
+ # line numbers.
86
+ _record_unique(
87
+ _id_locations(tree, _DECLARATION_RULES, file),
88
+ declared,
89
+ "variable",
90
+ validation_errors,
91
+ )
92
+ _record_unique(
93
+ _id_locations(tree, _NODE_RULES, file), node_ids, "node id", validation_errors
94
+ )
95
+ gotos.extend(_id_locations(tree, _GOTO_RULES, file))
96
+ validation_errors.extend(_number_range_errors(tree, file))
97
+ declarations.extend(data.get("declarations", []))
98
+
99
+ outputs.append((out_file, {"nodes": data["nodes"]}))
100
+
101
+ if failed:
102
+ raise typer.Exit(code=1)
103
+
104
+ validation_errors.extend(
105
+ (location, f"goto target '{target}' matches no node")
106
+ for target, location in gotos
107
+ if target not in node_ids
108
+ )
109
+ if validation_errors:
110
+ for (file, line, column), message in sorted(validation_errors):
111
+ report_error(message, file, line, column)
112
+ raise typer.Exit(code=1)
113
+
114
+ file_count = len(outputs)
115
+ outputs.append((Path(ENV_BUILD_FILE), {"declarations": declarations}))
116
+ _replace_build_dir(build_dir, outputs, pretty)
117
+ return file_count
118
+
119
+
120
+ def _replace_build_dir(
121
+ build_dir: Path, outputs: list[tuple[Path, dict]], pretty: bool
122
+ ) -> None:
123
+ tmp_dir = build_dir.with_name(f".{build_dir.name}.tmp")
124
+ old_dir = build_dir.with_name(f".{build_dir.name}.old")
125
+ try:
126
+ for leftover in (tmp_dir, old_dir):
127
+ if leftover.exists():
128
+ shutil.rmtree(leftover)
129
+ for out_file, data in outputs:
130
+ _write_json(data, tmp_dir / out_file, pretty)
131
+ if build_dir.exists():
132
+ build_dir.rename(old_dir)
133
+ tmp_dir.rename(build_dir)
134
+ except OSError as e:
135
+ shutil.rmtree(tmp_dir, ignore_errors=True)
136
+ report_error(
137
+ f"could not update '{build_dir.as_posix()}': {e.strerror or e}. "
138
+ "Close any program using its files and build again."
139
+ )
140
+ raise typer.Exit(code=1)
141
+ shutil.rmtree(old_dir, ignore_errors=True)
142
+
143
+
144
+ def _id_locations(
145
+ tree: Tree, rules: frozenset[str], file: Path
146
+ ) -> list[tuple[str, Location]]:
147
+ """Return (id, location) for the leading ID of every matching rule, in source order."""
148
+ locations = []
149
+ for subtree in tree.iter_subtrees_topdown():
150
+ if subtree.data in rules:
151
+ id_token = subtree.children[0]
152
+ locations.append((str(id_token), (file, id_token.line, id_token.column)))
153
+ return locations
154
+
155
+
156
+ def _record_unique(
157
+ locations: list[tuple[str, Location]],
158
+ seen: dict[str, Location],
159
+ kind: str,
160
+ errors: list[tuple[Location, str]],
161
+ ) -> None:
162
+ for name, location in locations:
163
+ if name in seen:
164
+ prev_file, prev_line, _ = seen[name]
165
+ errors.append(
166
+ (
167
+ location,
168
+ f"duplicate {kind} '{name}', first declared at "
169
+ f"{prev_file.as_posix()}:{prev_line}",
170
+ )
171
+ )
172
+ else:
173
+ seen[name] = location
174
+
175
+
176
+ def _number_range_errors(tree: Tree, file: Path) -> list[tuple[Location, str]]:
177
+ errors = []
178
+ for declaration in tree.find_data("number_declaration"):
179
+ name_token, *numbers = declaration.children
180
+ minimum, maximum, value = (_number(child) for child in numbers)
181
+ location = (file, name_token.line, name_token.column)
182
+ name = str(name_token)
183
+
184
+ if minimum is not None and maximum is not None and minimum > maximum:
185
+ errors.append(
186
+ (location, f"number '{name}' has min {minimum:g} greater than max {maximum:g}")
187
+ )
188
+ continue
189
+
190
+ default = f"default {value:g}" if value is not None else "implicit default 0"
191
+ value = value or 0.0
192
+ if minimum is not None and value < minimum:
193
+ errors.append(
194
+ (location, f"number '{name}' has {default} below its min {minimum:g}")
195
+ )
196
+ elif maximum is not None and value > maximum:
197
+ errors.append(
198
+ (location, f"number '{name}' has {default} above its max {maximum:g}")
199
+ )
200
+ return errors
201
+
202
+
203
+ def _number(child: Tree | Token | None) -> float | None:
204
+ if child is None:
205
+ return None
206
+ if isinstance(child, Tree):
207
+ return -float(child.children[0])
208
+ return float(child)
209
+
210
+
211
+ def _write_json(data: dict, out_file: Path, pretty: bool) -> None:
212
+ out_file.parent.mkdir(parents=True, exist_ok=True)
213
+ if pretty:
214
+ out_file.write_text(json.dumps(data, indent=2), encoding=ENCODING)
215
+ else:
216
+ out_file.write_text(json.dumps(data, separators=(",", ":")), encoding=ENCODING)
217
+
218
+
219
+ def _load_grammar(grammar_file: str) -> str:
220
+ grammar = (
221
+ resources.files("weavly.resources")
222
+ .joinpath(grammar_file)
223
+ .read_text(encoding=ENCODING)
224
+ )
225
+ return grammar
226
+
227
+
228
+ def _build_parser(grammar: str, parser_type: str) -> Lark:
229
+ return Lark(grammar, parser=parser_type)
230
+
231
+
232
+ def _parse(parser: Lark, text: str, transformer: Transformer) -> dict:
233
+ tree = parser.parse(text)
234
+ data = transformer.transform(tree)
235
+ return data
236
+
237
+
238
+ def _report_encoding_error(error: UnicodeDecodeError, file: Path) -> None:
239
+ line = error.object[: error.start].count(b"\n") + 1
240
+ report_error("invalid encoding, files must be saved as UTF-8", file, line)
241
+
242
+
243
+ def _report_string_error(error: InvalidStringError, file: Path) -> None:
244
+ token = error.token
245
+ report_error(
246
+ f"invalid string {token.value}: {error.reason}", file, token.line, token.column
247
+ )
248
+
249
+
250
+ def _report_parse_error(
251
+ error: UnexpectedInput, file: Path, text: str, names: dict[str, str]
252
+ ) -> None:
253
+ message, details = describe_syntax_error(error, text, names)
254
+ report_error(message, file, error.line, error.column, details)