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 +21 -0
- weavly-0.1.0/PKG-INFO +77 -0
- weavly-0.1.0/README.md +50 -0
- weavly-0.1.0/pyproject.toml +60 -0
- weavly-0.1.0/setup.cfg +4 -0
- weavly-0.1.0/src/weavly/__init__.py +1 -0
- weavly-0.1.0/src/weavly/cli.py +82 -0
- weavly-0.1.0/src/weavly/parsing/__init__.py +7 -0
- weavly-0.1.0/src/weavly/parsing/parser.py +254 -0
- weavly-0.1.0/src/weavly/parsing/syntax_errors.py +145 -0
- weavly-0.1.0/src/weavly/parsing/wvl_transformer.py +282 -0
- weavly-0.1.0/src/weavly/reporting.py +24 -0
- weavly-0.1.0/src/weavly/resources/__init__.py +0 -0
- weavly-0.1.0/src/weavly/resources/wvl-grammar.lark +241 -0
- weavly-0.1.0/src/weavly.egg-info/PKG-INFO +77 -0
- weavly-0.1.0/src/weavly.egg-info/SOURCES.txt +27 -0
- weavly-0.1.0/src/weavly.egg-info/dependency_links.txt +1 -0
- weavly-0.1.0/src/weavly.egg-info/entry_points.txt +2 -0
- weavly-0.1.0/src/weavly.egg-info/requires.txt +2 -0
- weavly-0.1.0/src/weavly.egg-info/top_level.txt +1 -0
- weavly-0.1.0/tests/test_build_output.py +77 -0
- weavly-0.1.0/tests/test_cli.py +106 -0
- weavly-0.1.0/tests/test_env_merge.py +189 -0
- weavly-0.1.0/tests/test_file_errors.py +64 -0
- weavly-0.1.0/tests/test_grammar_smoke.py +10 -0
- weavly-0.1.0/tests/test_reporting.py +26 -0
- weavly-0.1.0/tests/test_string_errors.py +23 -0
- weavly-0.1.0/tests/test_syntax_errors.py +124 -0
- weavly-0.1.0/tests/test_wvl.py +51 -0
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 @@
|
|
|
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,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)
|