parol-pygen 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.
- parol_pygen-0.1.0/PKG-INFO +340 -0
- parol_pygen-0.1.0/README.md +315 -0
- parol_pygen-0.1.0/pyproject.toml +51 -0
- parol_pygen-0.1.0/setup.cfg +4 -0
- parol_pygen-0.1.0/src/parol_pygen/__init__.py +13 -0
- parol_pygen-0.1.0/src/parol_pygen/cli.py +199 -0
- parol_pygen-0.1.0/src/parol_pygen/diagnostics.py +16 -0
- parol_pygen-0.1.0/src/parol_pygen/generator.py +287 -0
- parol_pygen-0.1.0/src/parol_pygen/lalr_engine.py +100 -0
- parol_pygen-0.1.0/src/parol_pygen/llk_engine.py +149 -0
- parol_pygen-0.1.0/src/parol_pygen/loader.py +18 -0
- parol_pygen-0.1.0/src/parol_pygen/model.py +276 -0
- parol_pygen-0.1.0/src/parol_pygen/parser.py +45 -0
- parol_pygen-0.1.0/src/parol_pygen/scaffold.py +304 -0
- parol_pygen-0.1.0/src/parol_pygen/scanner_adapter.py +198 -0
- parol_pygen-0.1.0/src/parol_pygen/schemas/__init__.py +1 -0
- parol_pygen-0.1.0/src/parol_pygen/schemas/parser-export-model.v1.schema.json +690 -0
- parol_pygen-0.1.0/src/parol_pygen/semantic_actions.py +68 -0
- parol_pygen-0.1.0/src/parol_pygen/table_compiler.py +41 -0
- parol_pygen-0.1.0/src/parol_pygen/validator.py +103 -0
- parol_pygen-0.1.0/src/parol_pygen.egg-info/PKG-INFO +340 -0
- parol_pygen-0.1.0/src/parol_pygen.egg-info/SOURCES.txt +27 -0
- parol_pygen-0.1.0/src/parol_pygen.egg-info/dependency_links.txt +1 -0
- parol_pygen-0.1.0/src/parol_pygen.egg-info/entry_points.txt +2 -0
- parol_pygen-0.1.0/src/parol_pygen.egg-info/requires.txt +6 -0
- parol_pygen-0.1.0/src/parol_pygen.egg-info/top_level.txt +1 -0
- parol_pygen-0.1.0/tests/test_cli.py +549 -0
- parol_pygen-0.1.0/tests/test_poc.py +224 -0
- parol_pygen-0.1.0/tests/test_scanner_definition.py +90 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parol-pygen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python parser generator/runtime consuming parol export JSON
|
|
5
|
+
Author: parol contributors
|
|
6
|
+
License-Expression: MIT OR Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/jsinger67/parol
|
|
8
|
+
Project-URL: Repository, https://github.com/jsinger67/parol
|
|
9
|
+
Project-URL: Issues, https://github.com/jsinger67/parol/issues
|
|
10
|
+
Keywords: parser,generator,lalr,llk,parol
|
|
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: Topic :: Software Development :: Compilers
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: jsonschema>=4.21.0
|
|
21
|
+
Requires-Dist: scnr2
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2.1; extra == "dev"
|
|
24
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# parol-pygen
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
Python parser runtime/generator consuming `parol export` JSON.
|
|
31
|
+
|
|
32
|
+
## Scope
|
|
33
|
+
|
|
34
|
+
- Supports `ParserExportModel` version `1`
|
|
35
|
+
- Supports export model validation for `algorithm = "Lalr1" | "Llk"`
|
|
36
|
+
- Runtime parsing supports `Lalr1` and `Llk`
|
|
37
|
+
- Uses `scnr2` Python binding as default scanner backend
|
|
38
|
+
- Supports regex fallback scanner via `--no-scnr2`
|
|
39
|
+
- Runs LALR shift/reduce and LLK predictive parse loops
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Export parser metadata JSON from a grammar file (example):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
parol export --grammar-file ./my-grammar-exp.par --output-file ./export.json --pretty
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then run `parol-pygen` commands against `./export.json`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python -m parol_pygen.cli --version
|
|
53
|
+
python -m parol_pygen.cli info
|
|
54
|
+
python -m parol_pygen.cli validate --export ./export.json
|
|
55
|
+
python -m parol_pygen.cli generate --export ./export.json --out . --package arg_generated
|
|
56
|
+
python -c "from arg_generated import Parser, UserActions; print(Parser(actions=UserActions()).parse('Var abc End').accepted)"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For a ready-to-run proof/project scaffold (scripts + runner + action skeleton):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m parol_pygen.cli init --out ./demo-proof --project "Demo Proof" --package demo_generated --export demo_export.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This creates a standalone project skeleton in `./demo-proof` with:
|
|
66
|
+
|
|
67
|
+
- `scripts/bootstrap.ps1`
|
|
68
|
+
- `scripts/generate-parser.ps1`
|
|
69
|
+
- `scripts/run-proof.ps1`
|
|
70
|
+
- `proof_runner.py`
|
|
71
|
+
- `custom_actions.py`
|
|
72
|
+
- `sample.txt`
|
|
73
|
+
|
|
74
|
+
Note: these are generated files inside the scaffold output directory (for example `./demo-proof/scripts/...`),
|
|
75
|
+
not maintainer scripts in this repository root.
|
|
76
|
+
|
|
77
|
+
The recommended flow is generation-first:
|
|
78
|
+
|
|
79
|
+
1. Validate export JSON (`validate`)
|
|
80
|
+
2. Generate a Python package (`generate`)
|
|
81
|
+
3. Import the generated `Parser`/`UserActions` from that package and parse input
|
|
82
|
+
|
|
83
|
+
Alternative: use `init` first when you want a full project scaffold instead of only a generated package.
|
|
84
|
+
|
|
85
|
+
The direct runtime command still exists for low-level diagnostics:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python -m parol_pygen.cli run --export ./export.json --text "Var abc End"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Replace `./export.json` with any export file produced by your `parol export` workflow.
|
|
92
|
+
|
|
93
|
+
`run` supports both `Lalr1` and `Llk` exports.
|
|
94
|
+
|
|
95
|
+
The `info` command prints JSON metadata (version, contract revision, model contract identifier,
|
|
96
|
+
schema id, capabilities, supported algorithm/export model, commands,
|
|
97
|
+
and error exit code conventions),
|
|
98
|
+
which is useful for tooling integration and diagnostics.
|
|
99
|
+
|
|
100
|
+
Compatibility rule for tooling consumers:
|
|
101
|
+
|
|
102
|
+
- If `contract_revision` is unknown and not listed in `supported_contract_revisions`,
|
|
103
|
+
treat the payload as potentially incompatible and reject by default.
|
|
104
|
+
|
|
105
|
+
Example integration check (fail closed by default):
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import json
|
|
109
|
+
import subprocess
|
|
110
|
+
import sys
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
cp = subprocess.run(
|
|
114
|
+
[sys.executable, "-m", "parol_pygen.cli", "info"],
|
|
115
|
+
text=True,
|
|
116
|
+
capture_output=True,
|
|
117
|
+
check=True,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
info = json.loads(cp.stdout)
|
|
121
|
+
revision = int(info["contract_revision"])
|
|
122
|
+
supported = {int(x) for x in info["supported_contract_revisions"]}
|
|
123
|
+
|
|
124
|
+
if revision not in supported:
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"Unsupported parol-pygen info contract revision {revision}; "
|
|
127
|
+
f"supported revisions: {sorted(supported)}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
print("Compatible info contract detected")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
PowerShell variant (Windows):
|
|
134
|
+
|
|
135
|
+
```powershell
|
|
136
|
+
$raw = python -m parol_pygen.cli info
|
|
137
|
+
$info = $raw | ConvertFrom-Json
|
|
138
|
+
|
|
139
|
+
$revision = [int]$info.contract_revision
|
|
140
|
+
$supported = @($info.supported_contract_revisions | ForEach-Object { [int]$_ })
|
|
141
|
+
|
|
142
|
+
if ($supported -notcontains $revision) {
|
|
143
|
+
throw "Unsupported parol-pygen info contract revision $revision; supported revisions: $($supported -join ', ')"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Write-Output "Compatible info contract detected"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
cmd.exe variant (Windows):
|
|
150
|
+
|
|
151
|
+
```bat
|
|
152
|
+
for /f "delims=" %I in ('python -m parol_pygen.cli info') do @set PAROL_INFO=%I
|
|
153
|
+
python -c "import json,os,sys; i=json.loads(os.environ['PAROL_INFO']); r=int(i['contract_revision']); s={int(x) for x in i['supported_contract_revisions']}; sys.exit(0 if r in s else 1)"
|
|
154
|
+
if errorlevel 1 (
|
|
155
|
+
echo Unsupported parol-pygen info contract revision
|
|
156
|
+
exit /b 1
|
|
157
|
+
) else (
|
|
158
|
+
echo Compatible info contract detected
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Invalid input returns a non-zero exit code and prints parse diagnostics to stderr:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python -m parol_pygen.cli run --export ./export.json --text "Var abc"
|
|
166
|
+
echo $?
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Exit code conventions used by the CLI:
|
|
170
|
+
|
|
171
|
+
- `0`: success
|
|
172
|
+
- `2`: user/input/data errors (invalid input text, invalid export JSON/schema, missing files)
|
|
173
|
+
- `1`: internal/unexpected failures
|
|
174
|
+
|
|
175
|
+
Use `--verbose-errors` to include exception type names in stderr output:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python -m parol_pygen.cli --verbose-errors run --export ./export.json --text "Var abc"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Development with uv
|
|
182
|
+
|
|
183
|
+
For local development and CI parity, prefer `uv` as package/environment manager.
|
|
184
|
+
|
|
185
|
+
Initial setup:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
uv venv
|
|
189
|
+
uv sync
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Run CLI commands in the managed environment:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
uv run python -m parol_pygen.cli --version
|
|
196
|
+
uv run python -m parol_pygen.cli info
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Run tests:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
uv run python -m unittest discover -s tests -p "test_*.py"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Build artifacts:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
uv sync --extra dev
|
|
209
|
+
uv run python -m build
|
|
210
|
+
uv run python -m twine check dist/*
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Release preflight:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
uv run pwsh ./scripts/check-changelog-entry.ps1
|
|
217
|
+
# optional explicit target version
|
|
218
|
+
uv run pwsh ./scripts/check-changelog-entry.ps1 -Version 0.1.0
|
|
219
|
+
|
|
220
|
+
# one-command local release dry-run (gate + build + artifact check + scaffold smoke)
|
|
221
|
+
uv run pwsh ./scripts/release-dry-run.ps1 -Version 0.1.0 -Execute
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This verifies that the current `pyproject.toml` version has a matching entry in `CHANGELOG.md`
|
|
225
|
+
before publish workflows are started.
|
|
226
|
+
|
|
227
|
+
## Release checklist (maintainers)
|
|
228
|
+
|
|
229
|
+
Suggested command order for cutting a release `X.Y.Z`:
|
|
230
|
+
|
|
231
|
+
```powershell
|
|
232
|
+
./scripts/check-changelog-entry.ps1 -Version X.Y.Z
|
|
233
|
+
./scripts/release-dry-run.ps1 -Version X.Y.Z -Execute
|
|
234
|
+
git add pyproject.toml CHANGELOG.md
|
|
235
|
+
git commit -m "chore(release): cut vX.Y.Z"
|
|
236
|
+
git tag -a vX.Y.Z -m "parol-pygen vX.Y.Z"
|
|
237
|
+
git push origin main
|
|
238
|
+
git push origin vX.Y.Z
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Recommended publish flow:
|
|
242
|
+
|
|
243
|
+
1. Publish to TestPyPI first.
|
|
244
|
+
2. Run smoke/install checks from a clean environment.
|
|
245
|
+
3. Publish to PyPI only after TestPyPI checks are green.
|
|
246
|
+
|
|
247
|
+
### Trusted Publishing setup (PyPI/TestPyPI)
|
|
248
|
+
|
|
249
|
+
This repository uses GitHub OIDC Trusted Publishing in
|
|
250
|
+
`.github/workflows/publish.yml` (`id-token: write` + `pypa/gh-action-pypi-publish`).
|
|
251
|
+
No API token secrets are required for publish jobs.
|
|
252
|
+
|
|
253
|
+
Configure publishers in both package indexes:
|
|
254
|
+
|
|
255
|
+
1. In TestPyPI project settings, add a Trusted Publisher:
|
|
256
|
+
- owner/repo: `jsinger67/parol-pygen`
|
|
257
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
258
|
+
- environment: `testpypi`
|
|
259
|
+
2. In PyPI project settings, add a Trusted Publisher:
|
|
260
|
+
- owner/repo: `jsinger67/parol-pygen`
|
|
261
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
262
|
+
- environment: `pypi`
|
|
263
|
+
|
|
264
|
+
Recommended first verification:
|
|
265
|
+
|
|
266
|
+
1. Run workflow dispatch with `repository=testpypi` and `version=X.Y.Z`.
|
|
267
|
+
2. Confirm successful TestPyPI publish.
|
|
268
|
+
3. Run with `repository=pypi` from the commit tagged `vX.Y.Z`.
|
|
269
|
+
|
|
270
|
+
## Semantic actions (parol-style)
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from arg_generated import Parser, UserActions
|
|
274
|
+
from arg_generated.nodes import NonTerminalNode, StartListNode
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class Actions(UserActions):
|
|
278
|
+
def on_start_list(self, node: StartListNode):
|
|
279
|
+
# Typed callback for reduced non-terminal StartList.
|
|
280
|
+
return {"kind": "StartList", "children": node.children}
|
|
281
|
+
|
|
282
|
+
def on_non_terminal(self, name: str, node: NonTerminalNode):
|
|
283
|
+
# Optional generic hook for all non-terminals.
|
|
284
|
+
return node
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
parser = Parser(actions=Actions())
|
|
288
|
+
result = parser.parse("Var abc End")
|
|
289
|
+
print(result.accepted)
|
|
290
|
+
print(result.value)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
This mirrors the standalone Pascal proof setup:
|
|
294
|
+
|
|
295
|
+
- generate package once (for example `pascal_generated`)
|
|
296
|
+
- subclass generated `UserActions`
|
|
297
|
+
- override typed `on_<non_terminal>` methods
|
|
298
|
+
- parse through generated `Parser`
|
|
299
|
+
|
|
300
|
+
Callback dispatch remains name-based (`on_<non_terminal_in_snake_case>`), and callback
|
|
301
|
+
return values are pushed as reduced semantic values.
|
|
302
|
+
|
|
303
|
+
Note: depending on parse table shape (augmented start handling), the start symbol itself may
|
|
304
|
+
not always appear as a user-visible callback.
|
|
305
|
+
|
|
306
|
+
Advanced: a low-level `on_production(lhs_nt, prod_idx, rhs_values)` hook is available for
|
|
307
|
+
algorithm-neutral internal experiments. `on_reduce(...)` remains supported as a
|
|
308
|
+
backward-compatible alias; see `MIGRATION.md` for upgrade guidance.
|
|
309
|
+
|
|
310
|
+
## Generated API Architecture
|
|
311
|
+
|
|
312
|
+
`parol-pygen generate` now provides a C#-style generated API surface for Python consumers.
|
|
313
|
+
The generated package is intended to be edited/extended by user code, while parser internals
|
|
314
|
+
remain in the reusable runtime.
|
|
315
|
+
|
|
316
|
+
Generated files:
|
|
317
|
+
|
|
318
|
+
- `parser.py`
|
|
319
|
+
- Generated parser facade
|
|
320
|
+
- Adapts runtime reduction payloads to generated typed node classes
|
|
321
|
+
- Dispatches typed semantic action callbacks
|
|
322
|
+
- `nodes.py`
|
|
323
|
+
- Generated concrete node classes (`<NonTerminal>Node`) using `@dataclass`
|
|
324
|
+
- Shared base types for generic and non-terminal nodes
|
|
325
|
+
- `actions.py`
|
|
326
|
+
- Generated action protocol with typed method signatures per non-terminal
|
|
327
|
+
- Base class with generic `on_non_terminal(name, node)` fallback hook
|
|
328
|
+
- `user_actions.py`
|
|
329
|
+
- Editable starter implementation intended for user semantic logic
|
|
330
|
+
- `export.json`
|
|
331
|
+
- Embedded parser export model used by the generated parser facade
|
|
332
|
+
|
|
333
|
+
Semantic action contract:
|
|
334
|
+
|
|
335
|
+
- Preferred method shape: `on_<non_terminal_in_snake_case>(node: <GeneratedNodeType>) -> Any`
|
|
336
|
+
- Generic fallback: `on_non_terminal(name: str, node: NonTerminalNode) -> Any`
|
|
337
|
+
|
|
338
|
+
Compatibility contract for tooling remains machine-readable through `parol-pygen info`
|
|
339
|
+
(`contract_revision`, `supported_contract_revisions`, `model_contract`, `schema_id`,
|
|
340
|
+
`capabilities`). Consumers should continue to fail closed on unknown revisions.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# parol-pygen
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Python parser runtime/generator consuming `parol export` JSON.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
- Supports `ParserExportModel` version `1`
|
|
10
|
+
- Supports export model validation for `algorithm = "Lalr1" | "Llk"`
|
|
11
|
+
- Runtime parsing supports `Lalr1` and `Llk`
|
|
12
|
+
- Uses `scnr2` Python binding as default scanner backend
|
|
13
|
+
- Supports regex fallback scanner via `--no-scnr2`
|
|
14
|
+
- Runs LALR shift/reduce and LLK predictive parse loops
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
Export parser metadata JSON from a grammar file (example):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
parol export --grammar-file ./my-grammar-exp.par --output-file ./export.json --pretty
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then run `parol-pygen` commands against `./export.json`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python -m parol_pygen.cli --version
|
|
28
|
+
python -m parol_pygen.cli info
|
|
29
|
+
python -m parol_pygen.cli validate --export ./export.json
|
|
30
|
+
python -m parol_pygen.cli generate --export ./export.json --out . --package arg_generated
|
|
31
|
+
python -c "from arg_generated import Parser, UserActions; print(Parser(actions=UserActions()).parse('Var abc End').accepted)"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For a ready-to-run proof/project scaffold (scripts + runner + action skeleton):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m parol_pygen.cli init --out ./demo-proof --project "Demo Proof" --package demo_generated --export demo_export.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This creates a standalone project skeleton in `./demo-proof` with:
|
|
41
|
+
|
|
42
|
+
- `scripts/bootstrap.ps1`
|
|
43
|
+
- `scripts/generate-parser.ps1`
|
|
44
|
+
- `scripts/run-proof.ps1`
|
|
45
|
+
- `proof_runner.py`
|
|
46
|
+
- `custom_actions.py`
|
|
47
|
+
- `sample.txt`
|
|
48
|
+
|
|
49
|
+
Note: these are generated files inside the scaffold output directory (for example `./demo-proof/scripts/...`),
|
|
50
|
+
not maintainer scripts in this repository root.
|
|
51
|
+
|
|
52
|
+
The recommended flow is generation-first:
|
|
53
|
+
|
|
54
|
+
1. Validate export JSON (`validate`)
|
|
55
|
+
2. Generate a Python package (`generate`)
|
|
56
|
+
3. Import the generated `Parser`/`UserActions` from that package and parse input
|
|
57
|
+
|
|
58
|
+
Alternative: use `init` first when you want a full project scaffold instead of only a generated package.
|
|
59
|
+
|
|
60
|
+
The direct runtime command still exists for low-level diagnostics:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -m parol_pygen.cli run --export ./export.json --text "Var abc End"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Replace `./export.json` with any export file produced by your `parol export` workflow.
|
|
67
|
+
|
|
68
|
+
`run` supports both `Lalr1` and `Llk` exports.
|
|
69
|
+
|
|
70
|
+
The `info` command prints JSON metadata (version, contract revision, model contract identifier,
|
|
71
|
+
schema id, capabilities, supported algorithm/export model, commands,
|
|
72
|
+
and error exit code conventions),
|
|
73
|
+
which is useful for tooling integration and diagnostics.
|
|
74
|
+
|
|
75
|
+
Compatibility rule for tooling consumers:
|
|
76
|
+
|
|
77
|
+
- If `contract_revision` is unknown and not listed in `supported_contract_revisions`,
|
|
78
|
+
treat the payload as potentially incompatible and reject by default.
|
|
79
|
+
|
|
80
|
+
Example integration check (fail closed by default):
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import json
|
|
84
|
+
import subprocess
|
|
85
|
+
import sys
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
cp = subprocess.run(
|
|
89
|
+
[sys.executable, "-m", "parol_pygen.cli", "info"],
|
|
90
|
+
text=True,
|
|
91
|
+
capture_output=True,
|
|
92
|
+
check=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
info = json.loads(cp.stdout)
|
|
96
|
+
revision = int(info["contract_revision"])
|
|
97
|
+
supported = {int(x) for x in info["supported_contract_revisions"]}
|
|
98
|
+
|
|
99
|
+
if revision not in supported:
|
|
100
|
+
raise RuntimeError(
|
|
101
|
+
f"Unsupported parol-pygen info contract revision {revision}; "
|
|
102
|
+
f"supported revisions: {sorted(supported)}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print("Compatible info contract detected")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
PowerShell variant (Windows):
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
$raw = python -m parol_pygen.cli info
|
|
112
|
+
$info = $raw | ConvertFrom-Json
|
|
113
|
+
|
|
114
|
+
$revision = [int]$info.contract_revision
|
|
115
|
+
$supported = @($info.supported_contract_revisions | ForEach-Object { [int]$_ })
|
|
116
|
+
|
|
117
|
+
if ($supported -notcontains $revision) {
|
|
118
|
+
throw "Unsupported parol-pygen info contract revision $revision; supported revisions: $($supported -join ', ')"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Write-Output "Compatible info contract detected"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
cmd.exe variant (Windows):
|
|
125
|
+
|
|
126
|
+
```bat
|
|
127
|
+
for /f "delims=" %I in ('python -m parol_pygen.cli info') do @set PAROL_INFO=%I
|
|
128
|
+
python -c "import json,os,sys; i=json.loads(os.environ['PAROL_INFO']); r=int(i['contract_revision']); s={int(x) for x in i['supported_contract_revisions']}; sys.exit(0 if r in s else 1)"
|
|
129
|
+
if errorlevel 1 (
|
|
130
|
+
echo Unsupported parol-pygen info contract revision
|
|
131
|
+
exit /b 1
|
|
132
|
+
) else (
|
|
133
|
+
echo Compatible info contract detected
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Invalid input returns a non-zero exit code and prints parse diagnostics to stderr:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
python -m parol_pygen.cli run --export ./export.json --text "Var abc"
|
|
141
|
+
echo $?
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Exit code conventions used by the CLI:
|
|
145
|
+
|
|
146
|
+
- `0`: success
|
|
147
|
+
- `2`: user/input/data errors (invalid input text, invalid export JSON/schema, missing files)
|
|
148
|
+
- `1`: internal/unexpected failures
|
|
149
|
+
|
|
150
|
+
Use `--verbose-errors` to include exception type names in stderr output:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python -m parol_pygen.cli --verbose-errors run --export ./export.json --text "Var abc"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Development with uv
|
|
157
|
+
|
|
158
|
+
For local development and CI parity, prefer `uv` as package/environment manager.
|
|
159
|
+
|
|
160
|
+
Initial setup:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv venv
|
|
164
|
+
uv sync
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Run CLI commands in the managed environment:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv run python -m parol_pygen.cli --version
|
|
171
|
+
uv run python -m parol_pygen.cli info
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Run tests:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv run python -m unittest discover -s tests -p "test_*.py"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Build artifacts:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv sync --extra dev
|
|
184
|
+
uv run python -m build
|
|
185
|
+
uv run python -m twine check dist/*
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Release preflight:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
uv run pwsh ./scripts/check-changelog-entry.ps1
|
|
192
|
+
# optional explicit target version
|
|
193
|
+
uv run pwsh ./scripts/check-changelog-entry.ps1 -Version 0.1.0
|
|
194
|
+
|
|
195
|
+
# one-command local release dry-run (gate + build + artifact check + scaffold smoke)
|
|
196
|
+
uv run pwsh ./scripts/release-dry-run.ps1 -Version 0.1.0 -Execute
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This verifies that the current `pyproject.toml` version has a matching entry in `CHANGELOG.md`
|
|
200
|
+
before publish workflows are started.
|
|
201
|
+
|
|
202
|
+
## Release checklist (maintainers)
|
|
203
|
+
|
|
204
|
+
Suggested command order for cutting a release `X.Y.Z`:
|
|
205
|
+
|
|
206
|
+
```powershell
|
|
207
|
+
./scripts/check-changelog-entry.ps1 -Version X.Y.Z
|
|
208
|
+
./scripts/release-dry-run.ps1 -Version X.Y.Z -Execute
|
|
209
|
+
git add pyproject.toml CHANGELOG.md
|
|
210
|
+
git commit -m "chore(release): cut vX.Y.Z"
|
|
211
|
+
git tag -a vX.Y.Z -m "parol-pygen vX.Y.Z"
|
|
212
|
+
git push origin main
|
|
213
|
+
git push origin vX.Y.Z
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Recommended publish flow:
|
|
217
|
+
|
|
218
|
+
1. Publish to TestPyPI first.
|
|
219
|
+
2. Run smoke/install checks from a clean environment.
|
|
220
|
+
3. Publish to PyPI only after TestPyPI checks are green.
|
|
221
|
+
|
|
222
|
+
### Trusted Publishing setup (PyPI/TestPyPI)
|
|
223
|
+
|
|
224
|
+
This repository uses GitHub OIDC Trusted Publishing in
|
|
225
|
+
`.github/workflows/publish.yml` (`id-token: write` + `pypa/gh-action-pypi-publish`).
|
|
226
|
+
No API token secrets are required for publish jobs.
|
|
227
|
+
|
|
228
|
+
Configure publishers in both package indexes:
|
|
229
|
+
|
|
230
|
+
1. In TestPyPI project settings, add a Trusted Publisher:
|
|
231
|
+
- owner/repo: `jsinger67/parol-pygen`
|
|
232
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
233
|
+
- environment: `testpypi`
|
|
234
|
+
2. In PyPI project settings, add a Trusted Publisher:
|
|
235
|
+
- owner/repo: `jsinger67/parol-pygen`
|
|
236
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
237
|
+
- environment: `pypi`
|
|
238
|
+
|
|
239
|
+
Recommended first verification:
|
|
240
|
+
|
|
241
|
+
1. Run workflow dispatch with `repository=testpypi` and `version=X.Y.Z`.
|
|
242
|
+
2. Confirm successful TestPyPI publish.
|
|
243
|
+
3. Run with `repository=pypi` from the commit tagged `vX.Y.Z`.
|
|
244
|
+
|
|
245
|
+
## Semantic actions (parol-style)
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from arg_generated import Parser, UserActions
|
|
249
|
+
from arg_generated.nodes import NonTerminalNode, StartListNode
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class Actions(UserActions):
|
|
253
|
+
def on_start_list(self, node: StartListNode):
|
|
254
|
+
# Typed callback for reduced non-terminal StartList.
|
|
255
|
+
return {"kind": "StartList", "children": node.children}
|
|
256
|
+
|
|
257
|
+
def on_non_terminal(self, name: str, node: NonTerminalNode):
|
|
258
|
+
# Optional generic hook for all non-terminals.
|
|
259
|
+
return node
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
parser = Parser(actions=Actions())
|
|
263
|
+
result = parser.parse("Var abc End")
|
|
264
|
+
print(result.accepted)
|
|
265
|
+
print(result.value)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
This mirrors the standalone Pascal proof setup:
|
|
269
|
+
|
|
270
|
+
- generate package once (for example `pascal_generated`)
|
|
271
|
+
- subclass generated `UserActions`
|
|
272
|
+
- override typed `on_<non_terminal>` methods
|
|
273
|
+
- parse through generated `Parser`
|
|
274
|
+
|
|
275
|
+
Callback dispatch remains name-based (`on_<non_terminal_in_snake_case>`), and callback
|
|
276
|
+
return values are pushed as reduced semantic values.
|
|
277
|
+
|
|
278
|
+
Note: depending on parse table shape (augmented start handling), the start symbol itself may
|
|
279
|
+
not always appear as a user-visible callback.
|
|
280
|
+
|
|
281
|
+
Advanced: a low-level `on_production(lhs_nt, prod_idx, rhs_values)` hook is available for
|
|
282
|
+
algorithm-neutral internal experiments. `on_reduce(...)` remains supported as a
|
|
283
|
+
backward-compatible alias; see `MIGRATION.md` for upgrade guidance.
|
|
284
|
+
|
|
285
|
+
## Generated API Architecture
|
|
286
|
+
|
|
287
|
+
`parol-pygen generate` now provides a C#-style generated API surface for Python consumers.
|
|
288
|
+
The generated package is intended to be edited/extended by user code, while parser internals
|
|
289
|
+
remain in the reusable runtime.
|
|
290
|
+
|
|
291
|
+
Generated files:
|
|
292
|
+
|
|
293
|
+
- `parser.py`
|
|
294
|
+
- Generated parser facade
|
|
295
|
+
- Adapts runtime reduction payloads to generated typed node classes
|
|
296
|
+
- Dispatches typed semantic action callbacks
|
|
297
|
+
- `nodes.py`
|
|
298
|
+
- Generated concrete node classes (`<NonTerminal>Node`) using `@dataclass`
|
|
299
|
+
- Shared base types for generic and non-terminal nodes
|
|
300
|
+
- `actions.py`
|
|
301
|
+
- Generated action protocol with typed method signatures per non-terminal
|
|
302
|
+
- Base class with generic `on_non_terminal(name, node)` fallback hook
|
|
303
|
+
- `user_actions.py`
|
|
304
|
+
- Editable starter implementation intended for user semantic logic
|
|
305
|
+
- `export.json`
|
|
306
|
+
- Embedded parser export model used by the generated parser facade
|
|
307
|
+
|
|
308
|
+
Semantic action contract:
|
|
309
|
+
|
|
310
|
+
- Preferred method shape: `on_<non_terminal_in_snake_case>(node: <GeneratedNodeType>) -> Any`
|
|
311
|
+
- Generic fallback: `on_non_terminal(name: str, node: NonTerminalNode) -> Any`
|
|
312
|
+
|
|
313
|
+
Compatibility contract for tooling remains machine-readable through `parol-pygen info`
|
|
314
|
+
(`contract_revision`, `supported_contract_revisions`, `model_contract`, `schema_id`,
|
|
315
|
+
`capabilities`). Consumers should continue to fail closed on unknown revisions.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "parol-pygen"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python parser generator/runtime consuming parol export JSON"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT OR Apache-2.0"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "parol contributors" },
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
keywords = ["parser", "generator", "lalr", "llk", "parol"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Compilers",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"jsonschema>=4.21.0",
|
|
27
|
+
"scnr2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"build>=1.2.1",
|
|
33
|
+
"twine>=5.0.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/jsinger67/parol"
|
|
38
|
+
Repository = "https://github.com/jsinger67/parol"
|
|
39
|
+
Issues = "https://github.com/jsinger67/parol/issues"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
parol-pygen = "parol_pygen.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools]
|
|
45
|
+
package-dir = {"" = "src"}
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.package-data]
|
|
51
|
+
parol_pygen = ["schemas/*.json"]
|