decl-lang 0.3.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.
- decl_lang-0.3.0/LICENSE +21 -0
- decl_lang-0.3.0/PKG-INFO +111 -0
- decl_lang-0.3.0/README.md +80 -0
- decl_lang-0.3.0/decl/__init__.py +255 -0
- decl_lang-0.3.0/decl/_tree_sitter/__init__.py +11 -0
- decl_lang-0.3.0/decl/_tree_sitter/_binding.pyi +3 -0
- decl_lang-0.3.0/decl/_tree_sitter/binding.c +26 -0
- decl_lang-0.3.0/decl/_tree_sitter/src/parser.c +35905 -0
- decl_lang-0.3.0/decl/_tree_sitter/src/scanner.c +58 -0
- decl_lang-0.3.0/decl/_tree_sitter/src/tree_sitter/alloc.h +54 -0
- decl_lang-0.3.0/decl/_tree_sitter/src/tree_sitter/array.h +336 -0
- decl_lang-0.3.0/decl/_tree_sitter/src/tree_sitter/parser.h +286 -0
- decl_lang-0.3.0/decl/cli.py +22 -0
- decl_lang-0.3.0/decl/runtime/__init__.py +0 -0
- decl_lang-0.3.0/decl/runtime/__main__.py +8 -0
- decl_lang-0.3.0/decl/runtime/checker.py +676 -0
- decl_lang-0.3.0/decl/runtime/cli.py +383 -0
- decl_lang-0.3.0/decl/runtime/conformance.py +76 -0
- decl_lang-0.3.0/decl/runtime/engine.py +1745 -0
- decl_lang-0.3.0/decl/runtime/fmt.py +332 -0
- decl_lang-0.3.0/decl/runtime/infer.py +1278 -0
- decl_lang-0.3.0/decl/runtime/lsp.py +3880 -0
- decl_lang-0.3.0/decl/runtime/module.py +235 -0
- decl_lang-0.3.0/decl/runtime/package.py +272 -0
- decl_lang-0.3.0/decl/runtime/parse.py +632 -0
- decl_lang-0.3.0/decl/runtime/pipeline.py +85 -0
- decl_lang-0.3.0/decl/runtime/repl.py +633 -0
- decl_lang-0.3.0/decl/runtime/semantics.py +1552 -0
- decl_lang-0.3.0/decl/runtime/session.py +1794 -0
- decl_lang-0.3.0/decl/runtime/subsume.py +264 -0
- decl_lang-0.3.0/decl_lang.egg-info/PKG-INFO +111 -0
- decl_lang-0.3.0/decl_lang.egg-info/SOURCES.txt +39 -0
- decl_lang-0.3.0/decl_lang.egg-info/dependency_links.txt +1 -0
- decl_lang-0.3.0/decl_lang.egg-info/entry_points.txt +3 -0
- decl_lang-0.3.0/decl_lang.egg-info/requires.txt +6 -0
- decl_lang-0.3.0/decl_lang.egg-info/top_level.txt +1 -0
- decl_lang-0.3.0/pyproject.toml +80 -0
- decl_lang-0.3.0/setup.cfg +4 -0
- decl_lang-0.3.0/setup.py +37 -0
- decl_lang-0.3.0/tests/test_api.py +76 -0
- decl_lang-0.3.0/tests/test_corpus.py +31 -0
decl_lang-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 luuvish
|
|
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.
|
decl_lang-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: decl-lang
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Decl — a declarative language for describing, generating, and validating structured data: the decl CLI (check / evaluate / validate / fmt), the decl-lsp language server, and a Python API, implemented natively
|
|
5
|
+
Author: luuvish
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/luuvish/decl-lang
|
|
8
|
+
Project-URL: Repository, https://github.com/luuvish/decl-lang
|
|
9
|
+
Project-URL: Issues, https://github.com/luuvish/decl-lang/issues
|
|
10
|
+
Keywords: decl,declarative,schema,validation,configuration,json,language,lsp
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: C
|
|
20
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: tree-sitter>=0.25
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# decl
|
|
33
|
+
|
|
34
|
+
**Decl** is a declarative language for describing, generating, and
|
|
35
|
+
validating structured data — a JSON superset with a strong static type
|
|
36
|
+
system, constraints with first-class diagnostics, references, physical
|
|
37
|
+
quantities, generics, and modules. Pure, deterministic, terminating.
|
|
38
|
+
|
|
39
|
+
This package is a **native Python implementation** of the whole
|
|
40
|
+
language — the tree-sitter grammar compiled as a C extension plus a
|
|
41
|
+
pure-Python port of the static checker, the evaluator, packages, the
|
|
42
|
+
canonical formatter, and the language server, byte-identical to the
|
|
43
|
+
reference implementation. It ships the `decl` command-line tool, the
|
|
44
|
+
`decl-lsp` language server, and a small Python API. No Node.js is
|
|
45
|
+
involved.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install decl-lang # installs as decl-lang, imports as `decl` — the command's name and the module path (decl.runtime)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Command line
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
decl check schema.decl # parse + static checks (module-aware)
|
|
55
|
+
decl evaluate site.decl # the exported outputs -> JSON on stdout
|
|
56
|
+
decl evaluate site.decl --output site=site.json --output report # one document to a file, one to stdout
|
|
57
|
+
decl evaluate cfg.decl --input deployed=doc.json --output deployed # bind a document, emit its completed value
|
|
58
|
+
decl validate cfg.decl --input deployed=doc.json --expect-errors E4001
|
|
59
|
+
decl validate tests/validation # judge a fixture corpus
|
|
60
|
+
decl fmt --check src/*.decl # canonical formatting
|
|
61
|
+
decl repl site.decl # an interactive session: expressions, bindings, edits, undo
|
|
62
|
+
decl-lsp # stdio language server for editors
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Python API
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import decl
|
|
69
|
+
|
|
70
|
+
docs = decl.evaluate("site.decl") # {"site": {...}} — the exported outputs, by name
|
|
71
|
+
site = decl.evaluate("site.decl", outputs=["site"])["site"]
|
|
72
|
+
done = decl.evaluate("cfg.decl", inputs={"deployed": "doc.json"}, outputs=["deployed"])["deployed"]
|
|
73
|
+
problems = decl.check("schema.decl") # [] when clean
|
|
74
|
+
report = decl.validate(
|
|
75
|
+
"cfg.decl", inputs={"deployed": {"host": "h"}}
|
|
76
|
+
) # a document may be a value, not a file
|
|
77
|
+
text = decl.format_source("const x=1+2\n") # 'const x = 1 + 2\n'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The functions are the `decl` command line in its own vocabulary:
|
|
81
|
+
`inputs` binds documents by input name (a JSON file path, or the value
|
|
82
|
+
itself), `outputs` names the roots to return — outputs, or inputs bound
|
|
83
|
+
here or demanded through their fallback — and defaults to the entry
|
|
84
|
+
module's exported outputs. The npm package (`evaluate`, `check`,
|
|
85
|
+
`validate`, `formatSource`) and the Rust crate (`decl_lang::evaluate`, …)
|
|
86
|
+
offer the same functions with the same semantics.
|
|
87
|
+
|
|
88
|
+
Every call runs the same implementation as the CLI and returns its
|
|
89
|
+
machine-readable report; `decl.DeclError.diagnostics` carries the
|
|
90
|
+
diagnostics (`file`, `severity`, `code`, `id`, `path`, `message`) when
|
|
91
|
+
an operation fails.
|
|
92
|
+
|
|
93
|
+
## A taste of the language
|
|
94
|
+
|
|
95
|
+
```decl
|
|
96
|
+
type Service = {
|
|
97
|
+
name: /[a-z][a-z0-9-]*/
|
|
98
|
+
port?: 1024..65535 = 8080
|
|
99
|
+
replicas?: 1..64 = 1
|
|
100
|
+
endpoint = `${name}:${port}`
|
|
101
|
+
assert grpc_ports: name != "grpc" || port >= 9000
|
|
102
|
+
else warn `grpc convention is 9000+`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export output demo: Service[] = [
|
|
106
|
+
{ name: "gateway" }
|
|
107
|
+
{ name: "auth", port: 9001, replicas: 2 }
|
|
108
|
+
]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Specification, guide, and sources: https://github.com/luuvish/decl-lang
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# decl
|
|
2
|
+
|
|
3
|
+
**Decl** is a declarative language for describing, generating, and
|
|
4
|
+
validating structured data — a JSON superset with a strong static type
|
|
5
|
+
system, constraints with first-class diagnostics, references, physical
|
|
6
|
+
quantities, generics, and modules. Pure, deterministic, terminating.
|
|
7
|
+
|
|
8
|
+
This package is a **native Python implementation** of the whole
|
|
9
|
+
language — the tree-sitter grammar compiled as a C extension plus a
|
|
10
|
+
pure-Python port of the static checker, the evaluator, packages, the
|
|
11
|
+
canonical formatter, and the language server, byte-identical to the
|
|
12
|
+
reference implementation. It ships the `decl` command-line tool, the
|
|
13
|
+
`decl-lsp` language server, and a small Python API. No Node.js is
|
|
14
|
+
involved.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install decl-lang # installs as decl-lang, imports as `decl` — the command's name and the module path (decl.runtime)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Command line
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
decl check schema.decl # parse + static checks (module-aware)
|
|
24
|
+
decl evaluate site.decl # the exported outputs -> JSON on stdout
|
|
25
|
+
decl evaluate site.decl --output site=site.json --output report # one document to a file, one to stdout
|
|
26
|
+
decl evaluate cfg.decl --input deployed=doc.json --output deployed # bind a document, emit its completed value
|
|
27
|
+
decl validate cfg.decl --input deployed=doc.json --expect-errors E4001
|
|
28
|
+
decl validate tests/validation # judge a fixture corpus
|
|
29
|
+
decl fmt --check src/*.decl # canonical formatting
|
|
30
|
+
decl repl site.decl # an interactive session: expressions, bindings, edits, undo
|
|
31
|
+
decl-lsp # stdio language server for editors
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Python API
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import decl
|
|
38
|
+
|
|
39
|
+
docs = decl.evaluate("site.decl") # {"site": {...}} — the exported outputs, by name
|
|
40
|
+
site = decl.evaluate("site.decl", outputs=["site"])["site"]
|
|
41
|
+
done = decl.evaluate("cfg.decl", inputs={"deployed": "doc.json"}, outputs=["deployed"])["deployed"]
|
|
42
|
+
problems = decl.check("schema.decl") # [] when clean
|
|
43
|
+
report = decl.validate(
|
|
44
|
+
"cfg.decl", inputs={"deployed": {"host": "h"}}
|
|
45
|
+
) # a document may be a value, not a file
|
|
46
|
+
text = decl.format_source("const x=1+2\n") # 'const x = 1 + 2\n'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The functions are the `decl` command line in its own vocabulary:
|
|
50
|
+
`inputs` binds documents by input name (a JSON file path, or the value
|
|
51
|
+
itself), `outputs` names the roots to return — outputs, or inputs bound
|
|
52
|
+
here or demanded through their fallback — and defaults to the entry
|
|
53
|
+
module's exported outputs. The npm package (`evaluate`, `check`,
|
|
54
|
+
`validate`, `formatSource`) and the Rust crate (`decl_lang::evaluate`, …)
|
|
55
|
+
offer the same functions with the same semantics.
|
|
56
|
+
|
|
57
|
+
Every call runs the same implementation as the CLI and returns its
|
|
58
|
+
machine-readable report; `decl.DeclError.diagnostics` carries the
|
|
59
|
+
diagnostics (`file`, `severity`, `code`, `id`, `path`, `message`) when
|
|
60
|
+
an operation fails.
|
|
61
|
+
|
|
62
|
+
## A taste of the language
|
|
63
|
+
|
|
64
|
+
```decl
|
|
65
|
+
type Service = {
|
|
66
|
+
name: /[a-z][a-z0-9-]*/
|
|
67
|
+
port?: 1024..65535 = 8080
|
|
68
|
+
replicas?: 1..64 = 1
|
|
69
|
+
endpoint = `${name}:${port}`
|
|
70
|
+
assert grpc_ports: name != "grpc" || port >= 9000
|
|
71
|
+
else warn `grpc convention is 9000+`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export output demo: Service[] = [
|
|
75
|
+
{ name: "gateway" }
|
|
76
|
+
{ name: "auth", port: 9001, replicas: 2 }
|
|
77
|
+
]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Specification, guide, and sources: https://github.com/luuvish/decl-lang
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Python API for the Decl language.
|
|
2
|
+
|
|
3
|
+
Every operation runs the native Python implementation (``decl.runtime``),
|
|
4
|
+
held byte-identical to the reference implementation by
|
|
5
|
+
tests/parity/differential.py. The functions mirror the ``decl`` command
|
|
6
|
+
line, in its vocabulary: ``evaluate`` binds inputs and returns outputs.
|
|
7
|
+
|
|
8
|
+
>>> import decl
|
|
9
|
+
>>> decl.evaluate("site.decl") # {"site": {...}} — the exported outputs
|
|
10
|
+
>>> decl.evaluate("cfg.decl", inputs={"deployed": "doc.json"}, outputs=["deployed"])["deployed"]
|
|
11
|
+
>>> decl.check("schema.decl") # [] when clean, else diagnostics
|
|
12
|
+
>>> decl.validate("cfg.decl", inputs={"deployed": {"host": "h"}})
|
|
13
|
+
... # a document may be a value, not a file
|
|
14
|
+
>>> decl.format_source("const x=1+2\\n") # 'const x = 1 + 2\\n'
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
22
|
+
from typing import Any, TypedDict, cast
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DeclError",
|
|
26
|
+
"Diagnostic",
|
|
27
|
+
"check",
|
|
28
|
+
"evaluate",
|
|
29
|
+
"evaluate_source",
|
|
30
|
+
"format_file",
|
|
31
|
+
"format_source",
|
|
32
|
+
"validate",
|
|
33
|
+
]
|
|
34
|
+
__version__ = "0.3.0"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Diagnostic(TypedDict, total=False):
|
|
38
|
+
"""One diagnostic, in the report's field order (§12.2)."""
|
|
39
|
+
|
|
40
|
+
file: str
|
|
41
|
+
code: str
|
|
42
|
+
id: str
|
|
43
|
+
severity: str
|
|
44
|
+
message: str
|
|
45
|
+
path: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeclError(Exception):
|
|
49
|
+
"""Raised when an operation fails; ``diagnostics`` carries the report
|
|
50
|
+
(empty for a usage error such as an unknown input or root)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str, diagnostics: Sequence[Diagnostic] = ()):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.diagnostics: list[Diagnostic] = list(diagnostics)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# a document to bind to an input: the path of a JSON file, or the value itself
|
|
58
|
+
InputDocument = "str | os.PathLike[str] | Any"
|
|
59
|
+
Inputs = "Mapping[str, InputDocument] | Iterable[tuple[str, InputDocument]]"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _tagged(file: str, d: dict[str, Any]) -> Diagnostic:
|
|
63
|
+
o: dict[str, Any] = {"file": file}
|
|
64
|
+
if d.get("code"):
|
|
65
|
+
o["code"] = d["code"]
|
|
66
|
+
if d.get("id"):
|
|
67
|
+
o["id"] = d["id"]
|
|
68
|
+
o["severity"] = d["severity"]
|
|
69
|
+
o["message"] = d["message"]
|
|
70
|
+
o["path"] = d.get("path", "")
|
|
71
|
+
return cast(Diagnostic, o)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _fail(fallback: str, diagnostics: list[Any]) -> None:
|
|
75
|
+
raise DeclError(diagnostics[0]["message"] if diagnostics else fallback, diagnostics)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _file_tag(given: str, entry: Any, module_path: str) -> str:
|
|
79
|
+
return given if entry is not None and module_path == entry.path else module_path
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _pairs(inputs: Any) -> list[Any]:
|
|
83
|
+
if inputs is None:
|
|
84
|
+
return []
|
|
85
|
+
return list(inputs.items()) if isinstance(inputs, Mapping) else list(inputs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _bind_inputs(modules: list[Any], file: str, inputs: Any) -> list[Any]:
|
|
89
|
+
"""The documents to bind, each to the module that declares its input (§10)."""
|
|
90
|
+
from .runtime.semantics import read_json
|
|
91
|
+
|
|
92
|
+
binds = []
|
|
93
|
+
for name, doc in _pairs(inputs):
|
|
94
|
+
module = next((m for m in modules if name in m.env.inputs), None)
|
|
95
|
+
if module is None:
|
|
96
|
+
raise DeclError(f"no input named {name}")
|
|
97
|
+
if isinstance(doc, (str, os.PathLike)):
|
|
98
|
+
try:
|
|
99
|
+
with open(doc, encoding="utf-8") as fh:
|
|
100
|
+
text = fh.read()
|
|
101
|
+
except OSError:
|
|
102
|
+
_fail(
|
|
103
|
+
"",
|
|
104
|
+
[
|
|
105
|
+
{
|
|
106
|
+
"file": file,
|
|
107
|
+
"code": "E6004",
|
|
108
|
+
"severity": "error",
|
|
109
|
+
"message": f"bound document cannot be read: {doc}",
|
|
110
|
+
"path": name,
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
where = str(doc)
|
|
115
|
+
else:
|
|
116
|
+
text, where = json.dumps(doc), name
|
|
117
|
+
try:
|
|
118
|
+
raw = read_json(text)
|
|
119
|
+
except Exception:
|
|
120
|
+
_fail(
|
|
121
|
+
"",
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
"file": file,
|
|
125
|
+
"code": "E6004",
|
|
126
|
+
"severity": "error",
|
|
127
|
+
"message": f"bound document is not well-formed JSON: {where}",
|
|
128
|
+
"path": name,
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
binds.append({"module": module, "input": name, "raw": raw})
|
|
133
|
+
return binds
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def evaluate(
|
|
137
|
+
path: str | os.PathLike[str],
|
|
138
|
+
*,
|
|
139
|
+
inputs: Mapping[str, Any] | Iterable[tuple[str, Any]] | None = None,
|
|
140
|
+
outputs: Iterable[str] | None = None,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
"""Evaluate a module on the native runtime: bind the ``inputs`` documents
|
|
143
|
+
(by input name; a JSON file path, or the value itself), run the pipeline,
|
|
144
|
+
and return the requested roots' documents by name — ``outputs`` may name
|
|
145
|
+
outputs and inputs (bound here, or demanded through their fallback);
|
|
146
|
+
by default the entry module's exported outputs (§5.5). Raises DeclError
|
|
147
|
+
with the diagnostics on any error-severity outcome."""
|
|
148
|
+
from .runtime.checker import check_module
|
|
149
|
+
from .runtime.cli import open_universe
|
|
150
|
+
from .runtime.module import run_universe
|
|
151
|
+
|
|
152
|
+
file = str(path)
|
|
153
|
+
r = open_universe(file)
|
|
154
|
+
if r["diags"] or r["entry"] is None:
|
|
155
|
+
_fail(f"{file}: cannot be loaded", [_tagged(file, d) for d in r["diags"]])
|
|
156
|
+
entry = r["entry"]
|
|
157
|
+
checks = [
|
|
158
|
+
_tagged(_file_tag(file, entry, m.path), d)
|
|
159
|
+
for m in r["modules"]
|
|
160
|
+
for d in check_module(m.decls, m.env)
|
|
161
|
+
]
|
|
162
|
+
if checks:
|
|
163
|
+
_fail("", checks)
|
|
164
|
+
u = run_universe(r["modules"], entry, _bind_inputs(r["modules"], file, inputs))
|
|
165
|
+
report = [_tagged(file, d) for d in u["diags"]]
|
|
166
|
+
if any(d["severity"] == "error" for d in report):
|
|
167
|
+
_fail("", report)
|
|
168
|
+
names = (
|
|
169
|
+
list(outputs)
|
|
170
|
+
if outputs is not None
|
|
171
|
+
else [o["name"] for o in entry.env.outputs if o.get("exported")]
|
|
172
|
+
)
|
|
173
|
+
out: dict[str, Any] = {}
|
|
174
|
+
for n in names:
|
|
175
|
+
if n not in entry.env.roots:
|
|
176
|
+
raise DeclError(f"no root named {n}", report)
|
|
177
|
+
out[n] = json.loads(u["eng"].serialize(entry.env.roots[n], n))
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check(*paths: str | os.PathLike[str]) -> list[Diagnostic]:
|
|
182
|
+
"""Parse and statically check entry files (module-aware) on the native
|
|
183
|
+
runtime. Returns diagnostics; empty means clean."""
|
|
184
|
+
from .runtime.cli import check_files
|
|
185
|
+
|
|
186
|
+
return [_tagged(d["file"], d) for d in check_files([str(p) for p in paths])]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def validate(
|
|
190
|
+
path: str | os.PathLike[str],
|
|
191
|
+
*,
|
|
192
|
+
inputs: Mapping[str, Any] | Iterable[tuple[str, Any]] | None = None,
|
|
193
|
+
expect_errors: Iterable[str] | None = None,
|
|
194
|
+
) -> list[Diagnostic]:
|
|
195
|
+
"""Validate a file: static checks, then evaluation with the ``inputs``
|
|
196
|
+
documents bound; returns every diagnostic (all severities). Raises
|
|
197
|
+
DeclError when the file does not parse. With ``expect_errors`` the set
|
|
198
|
+
of error codes must match exactly; DeclError carries the mismatch."""
|
|
199
|
+
from .runtime.checker import check_module
|
|
200
|
+
from .runtime.cli import open_universe
|
|
201
|
+
from .runtime.module import run_universe
|
|
202
|
+
from .runtime.parse import parse_source
|
|
203
|
+
from .runtime.pipeline import run_pipeline
|
|
204
|
+
|
|
205
|
+
file = str(path)
|
|
206
|
+
with open(file, encoding="utf-8") as fh:
|
|
207
|
+
src = fh.read()
|
|
208
|
+
parsed = parse_source(src)
|
|
209
|
+
if parsed["errors"]:
|
|
210
|
+
raise DeclError(f"{file}: {len(parsed['errors'])} parse error(s)")
|
|
211
|
+
decls = parsed["decls"]
|
|
212
|
+
diags = [_tagged(file, d) for d in check_module(decls)]
|
|
213
|
+
if not diags:
|
|
214
|
+
if _pairs(inputs):
|
|
215
|
+
r = open_universe(file)
|
|
216
|
+
u = run_universe(r["modules"], r["entry"], _bind_inputs(r["modules"], file, inputs))
|
|
217
|
+
diags = [_tagged(file, d) for d in u["diags"]]
|
|
218
|
+
else:
|
|
219
|
+
diags = [_tagged(file, d) for d in run_pipeline(decls)["diags"]]
|
|
220
|
+
if expect_errors is not None:
|
|
221
|
+
want = sorted(expect_errors)
|
|
222
|
+
got = sorted(d.get("code") or "" for d in diags if d["severity"] == "error")
|
|
223
|
+
if set(want) != set(got):
|
|
224
|
+
raise DeclError(f"expected errors {want}, got {got}", diags)
|
|
225
|
+
return diags
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def evaluate_source(text: str) -> dict[str, Any]:
|
|
229
|
+
"""Parse, check, and evaluate one module given as source text; returns
|
|
230
|
+
the report dict (phase, ok, parse_errors, checks, diagnostics, outputs, inputs)."""
|
|
231
|
+
from .runtime.pipeline import evaluate_source as run
|
|
232
|
+
|
|
233
|
+
return run(text)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def format_file(path: str | os.PathLike[str], *, check: bool = False) -> bool:
|
|
237
|
+
"""Canonically format a file in place. Returns True if it was (or would be) changed."""
|
|
238
|
+
with open(path, encoding="utf-8") as f:
|
|
239
|
+
src = f.read()
|
|
240
|
+
out = format_source(src)
|
|
241
|
+
if out != src and not check:
|
|
242
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
243
|
+
f.write(out)
|
|
244
|
+
return out != src
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def format_source(text: str) -> str:
|
|
248
|
+
"""Return the canonical formatting of a Decl source string; raises
|
|
249
|
+
DeclError when it does not parse."""
|
|
250
|
+
from .runtime.fmt import format_source as fmt
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
return fmt(text)
|
|
254
|
+
except ValueError as e:
|
|
255
|
+
raise DeclError(str(e)) from None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The Decl grammar as a `tree_sitter.Language` (compiled C extension)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tree_sitter import Language
|
|
6
|
+
|
|
7
|
+
from ._binding import language as _language_ptr
|
|
8
|
+
|
|
9
|
+
LANGUAGE = Language(_language_ptr())
|
|
10
|
+
|
|
11
|
+
__all__ = ["LANGUAGE"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Python binding for the Decl tree-sitter grammar: exposes the language
|
|
2
|
+
* as a PyCapsule the `tree_sitter` package accepts. The grammar sources
|
|
3
|
+
* (parser.c, scanner.c) are synced from ../../tree-sitter-decl/src. */
|
|
4
|
+
#include <Python.h>
|
|
5
|
+
|
|
6
|
+
typedef struct TSLanguage TSLanguage;
|
|
7
|
+
TSLanguage *tree_sitter_decl(void);
|
|
8
|
+
|
|
9
|
+
static PyObject *binding_language(PyObject *self, PyObject *args) {
|
|
10
|
+
(void)self; (void)args;
|
|
11
|
+
return PyCapsule_New(tree_sitter_decl(), "tree_sitter.Language", NULL);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static PyMethodDef methods[] = {
|
|
15
|
+
{"language", binding_language, METH_NOARGS, "Get the tree-sitter language for Decl."},
|
|
16
|
+
{NULL, NULL, 0, NULL}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
static struct PyModuleDef module = {
|
|
20
|
+
PyModuleDef_HEAD_INIT, "_binding", "Decl tree-sitter grammar", -1, methods,
|
|
21
|
+
NULL, NULL, NULL, NULL
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
PyMODINIT_FUNC PyInit__binding(void) {
|
|
25
|
+
return PyModule_Create(&module);
|
|
26
|
+
}
|