wunderspec 0.129.1__tar.gz → 0.129.2__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.
- {wunderspec-0.129.1 → wunderspec-0.129.2}/.flake8 +1 -0
- wunderspec-0.129.2/.github/workflows/build.yml +34 -0
- wunderspec-0.129.2/.github/workflows/convert-to-tla.yml +78 -0
- wunderspec-0.129.2/.github/workflows/lint.yml +40 -0
- wunderspec-0.129.2/.github/workflows/run-examples.yml +31 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/PKG-INFO +3 -3
- {wunderspec-0.129.1 → wunderspec-0.129.2}/README.md +2 -2
- {wunderspec-0.129.1 → wunderspec-0.129.2}/pyproject.toml +1 -1
- wunderspec-0.129.2/scripts/__init__.py +1 -0
- wunderspec-0.129.2/scripts/_spec_utils.py +190 -0
- wunderspec-0.129.2/scripts/convert_examples_to_tla.py +150 -0
- wunderspec-0.129.2/scripts/run_examples.py +163 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/tests/README.md +12 -12
- {wunderspec-0.129.1 → wunderspec-0.129.2}/uv.lock +1 -1
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/__init__.py +1 -1
- {wunderspec-0.129.1 → wunderspec-0.129.2}/.devcontainer/Dockerfile +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/.devcontainer/devcontainer.json +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/.devcontainer/post-create.sh +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/.github/workflows/publish.yml +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/.gitignore +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/CONTRIBUTING.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/LICENSE +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/README.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-1024.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-256.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-512.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-dark-1024.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-dark-512.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-transparent-1024.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-lockup-horizontal-dark-1600.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-lockup-horizontal-dark-800.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-mark-transparent-1024.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-readme-header-dark-1200x480.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-readme-header-dark-1600x640.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-social-preview-1280x640.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/png/wunderspec-wordmark-transparent-900.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-avatar-github.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-icon-circle-dark.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-icon-circle-transparent.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-lockup-horizontal-dark.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-lockup-horizontal-transparent.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-mark-transparent.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-readme-header-dark.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/assets/design/svg/wunderspec-wordmark-transparent.svg +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/basedpyrightconfig.json +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/references/from-quint-llms.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/references/from-tla-llms.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/booleans.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/cheatsheet.html +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/comprehensions.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/decorators.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/enums.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/flow.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/integers.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/lists.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/maps.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/records.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/sets.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/state-machine.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/strings.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/temporal.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/tuples.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-references/unions.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-stories/bobs_log.md +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-stories/bobs_log.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/docs/user-stories/img/bob.png +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/bags.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/bakery.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/bakery_walk.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/ben_or.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/channel.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/dekker.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/epfd.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/examples.yaml +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/fifo.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/fpaxos.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/kv_store.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/lamport_mutex.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/ledger.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/minimmit.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/payment.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/producer_consumer.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/readers_writers.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/readers_writers_walk.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/simple_ponzi.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/simple_ponzi_machine.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/simple_ponzi_machine_walk.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/simple_wal1.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/simple_wal2.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/sliding_puzzles.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/sloppy_counter.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/test_simple_ponzi_machine.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/tiny_workers.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/examples/two_phase.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/pyrightconfig.json +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/__main__.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/_edition.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/apalache.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/api.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/__init__.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/action_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/list_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/map_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/record_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/serialization.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/set_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/sorts.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/temporal_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/terms.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/tuple_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/ast/union_ast.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/cli.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/dev_debug.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/doc_format.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/errors.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/exec/__init__.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/exec/action_exec.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/exec/context.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/exec/scheduler.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/explain.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/expr.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/flow.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/fuzzer.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/interpreter.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/interpreter_sampling.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/interpreter_value.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/itf_trace.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/lang.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/linter.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/machine.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/model_checker.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/permutation.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/petnames.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/pretty.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/py.typed +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/quint_convert.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/random_walk.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/source_tracking.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/submachine.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/sym_context.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/tla.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/tlc.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/tlc_trace.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/trace_output.py +0 -0
- {wunderspec-0.129.1 → wunderspec-0.129.2}/wunderspec/uniq_names.py +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ main ]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [ main ]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
cache-suffix: build
|
|
26
|
+
|
|
27
|
+
- name: Build package
|
|
28
|
+
run: uv build
|
|
29
|
+
|
|
30
|
+
- name: Upload build artifacts
|
|
31
|
+
uses: actions/upload-artifact@v4
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Convert to TLA+
|
|
2
|
+
|
|
3
|
+
env:
|
|
4
|
+
APALACHE_VERSION: "0.57.0"
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
push:
|
|
9
|
+
branches: [ main ]
|
|
10
|
+
pull_request:
|
|
11
|
+
branches: [ main ]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
convert-to-tla:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
cache-suffix: convert-to-tla
|
|
29
|
+
|
|
30
|
+
- name: Install project
|
|
31
|
+
run: uv sync --locked
|
|
32
|
+
|
|
33
|
+
- name: Set up Java
|
|
34
|
+
uses: actions/setup-java@v5
|
|
35
|
+
with:
|
|
36
|
+
distribution: 'temurin'
|
|
37
|
+
java-version: '17'
|
|
38
|
+
|
|
39
|
+
- name: Cache tla2tools.jar
|
|
40
|
+
id: cache-tla2tools
|
|
41
|
+
uses: actions/cache@v5
|
|
42
|
+
with:
|
|
43
|
+
path: tla2tools.jar
|
|
44
|
+
key: tla2tools-v1.8.0
|
|
45
|
+
|
|
46
|
+
- name: Download tla2tools.jar
|
|
47
|
+
if: steps.cache-tla2tools.outputs.cache-hit != 'true'
|
|
48
|
+
run: wget -q https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar
|
|
49
|
+
|
|
50
|
+
- name: Cache Apalache
|
|
51
|
+
id: cache-apalache
|
|
52
|
+
uses: actions/cache@v5
|
|
53
|
+
with:
|
|
54
|
+
path: apalache-${{ env.APALACHE_VERSION }}
|
|
55
|
+
key: apalache-v${{ env.APALACHE_VERSION }}
|
|
56
|
+
|
|
57
|
+
- name: Download Apalache
|
|
58
|
+
if: steps.cache-apalache.outputs.cache-hit != 'true'
|
|
59
|
+
run: |
|
|
60
|
+
curl -fsSL "https://github.com/apalache-mc/apalache/releases/download/v${APALACHE_VERSION}/apalache-${APALACHE_VERSION}.tgz" -o apalache.tgz
|
|
61
|
+
tar -xzf apalache.tgz
|
|
62
|
+
rm apalache.tgz
|
|
63
|
+
|
|
64
|
+
- name: Add Apalache to PATH
|
|
65
|
+
run: echo "$PWD/apalache-${APALACHE_VERSION}/bin" >> "$GITHUB_PATH"
|
|
66
|
+
|
|
67
|
+
- name: Convert examples to TLA+
|
|
68
|
+
run: uv run python scripts/convert_examples_to_tla.py
|
|
69
|
+
|
|
70
|
+
- name: Run Apalache typecheck on generated specs
|
|
71
|
+
run: |
|
|
72
|
+
set -euo pipefail
|
|
73
|
+
mkdir -p .apalache-out/build
|
|
74
|
+
cd .build
|
|
75
|
+
for f in *.tla; do
|
|
76
|
+
echo "=== Typechecking $f ==="
|
|
77
|
+
apalache-mc typecheck --out-dir="../.apalache-out/build/$(basename "${f%.tla}")" "$f"
|
|
78
|
+
done
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ main ]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [ main ]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
cache-suffix: lint
|
|
26
|
+
|
|
27
|
+
- name: Install project
|
|
28
|
+
run: uv sync --locked --group dev
|
|
29
|
+
|
|
30
|
+
- name: Check code formatting with Black
|
|
31
|
+
run: uv run black --check --diff .
|
|
32
|
+
|
|
33
|
+
- name: Check import sorting with isort
|
|
34
|
+
run: uv run isort --check-only --diff .
|
|
35
|
+
|
|
36
|
+
- name: Lint with flake8
|
|
37
|
+
run: uv run flake8 wunderspec examples scripts
|
|
38
|
+
|
|
39
|
+
- name: Type check with mypy
|
|
40
|
+
run: uv run mypy wunderspec
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Run Examples
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ main ]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [ main ]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
run-examples:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
cache-suffix: run-examples
|
|
26
|
+
|
|
27
|
+
- name: Install project
|
|
28
|
+
run: uv sync --locked
|
|
29
|
+
|
|
30
|
+
- name: Run examples
|
|
31
|
+
run: uv run python scripts/run_examples.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wunderspec
|
|
3
|
-
Version: 0.129.
|
|
3
|
+
Version: 0.129.2
|
|
4
4
|
Summary: Protocol specifications as Python code
|
|
5
5
|
Author: Igor Konnov, Thomas Pani
|
|
6
6
|
License-File: LICENSE
|
|
@@ -74,8 +74,8 @@ commands are not included in this package.
|
|
|
74
74
|
|
|
75
75
|
## 4. Release Provenance
|
|
76
76
|
|
|
77
|
-
- Release tag: `v0.129.
|
|
78
|
-
- Source commit: `
|
|
77
|
+
- Release tag: `v0.129.2`
|
|
78
|
+
- Source commit: `f60e52f632281f5cbb2d1a382ea49b10750d2f4d`
|
|
79
79
|
|
|
80
80
|
See [tests/README.md](https://github.com/wunderspec/wunderspec/blob/main/tests/README.md)
|
|
81
81
|
for the development test log captured at release time.
|
|
@@ -58,8 +58,8 @@ commands are not included in this package.
|
|
|
58
58
|
|
|
59
59
|
## 4. Release Provenance
|
|
60
60
|
|
|
61
|
-
- Release tag: `v0.129.
|
|
62
|
-
- Source commit: `
|
|
61
|
+
- Release tag: `v0.129.2`
|
|
62
|
+
- Source commit: `f60e52f632281f5cbb2d1a382ea49b10750d2f4d`
|
|
63
63
|
|
|
64
64
|
See [tests/README.md](https://github.com/wunderspec/wunderspec/blob/main/tests/README.md)
|
|
65
65
|
for the development test log captured at release time.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Repository maintenance scripts."""
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Shared AST helpers for wunderspec example scripts."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, TypedDict
|
|
7
|
+
|
|
8
|
+
import yaml # type: ignore[import-untyped]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExampleConfigRow(TypedDict):
|
|
12
|
+
file: str
|
|
13
|
+
instances: list[str]
|
|
14
|
+
invariants_auto: bool
|
|
15
|
+
invariants: list[str]
|
|
16
|
+
examples_auto: bool
|
|
17
|
+
examples: list[str]
|
|
18
|
+
example_run_seeds: dict[str, int]
|
|
19
|
+
example_run_max_samples: dict[str, int]
|
|
20
|
+
timeout: int | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _find_decorated_names(spec_path: Path, decorator_id: str) -> list[str]:
|
|
24
|
+
"""Return names of functions decorated with ``@decorator_id`` in *spec_path*.
|
|
25
|
+
|
|
26
|
+
Uses AST inspection so the module is never imported (avoids dependency
|
|
27
|
+
issues with spec-local imports such as ``from simple_ponzi import *``).
|
|
28
|
+
"""
|
|
29
|
+
tree = ast.parse(spec_path.read_text())
|
|
30
|
+
names: list[str] = []
|
|
31
|
+
for node in ast.walk(tree):
|
|
32
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
33
|
+
for decorator in node.decorator_list:
|
|
34
|
+
if isinstance(decorator, ast.Name) and decorator.id == decorator_id:
|
|
35
|
+
names.append(node.name)
|
|
36
|
+
return names
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_invariant_names(spec_path: Path) -> set[str]:
|
|
40
|
+
"""Return the names of functions decorated with ``@invariant`` in *spec_path*."""
|
|
41
|
+
return set(_find_decorated_names(spec_path, "invariant"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def find_example_names(spec_path: Path) -> set[str]:
|
|
45
|
+
"""Return the names of functions decorated with ``@example`` in *spec_path*."""
|
|
46
|
+
return set(_find_decorated_names(spec_path, "example"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_coverage_names(spec_path: Path) -> set[str]:
|
|
50
|
+
"""Return the names of functions decorated with ``@coverage`` in *spec_path*."""
|
|
51
|
+
return set(_find_decorated_names(spec_path, "coverage"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_init_action_name(spec_path: Path) -> str:
|
|
55
|
+
"""Return the name of the ``@action(init=True)`` function, or ``"init"``."""
|
|
56
|
+
tree = ast.parse(spec_path.read_text())
|
|
57
|
+
for node in ast.walk(tree):
|
|
58
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
59
|
+
for decorator in node.decorator_list:
|
|
60
|
+
if isinstance(decorator, ast.Call):
|
|
61
|
+
func = decorator.func
|
|
62
|
+
if isinstance(func, ast.Name) and func.id == "action":
|
|
63
|
+
for kw in decorator.keywords:
|
|
64
|
+
if (
|
|
65
|
+
kw.arg == "init"
|
|
66
|
+
and isinstance(kw.value, ast.Constant)
|
|
67
|
+
and kw.value.value is True
|
|
68
|
+
):
|
|
69
|
+
return node.name
|
|
70
|
+
return "init"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_STEP_NAME_RE = re.compile(r"^(Next|step|Step|.*_next|.*Next)$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def find_step_action_name(spec_path: Path, init_name: str) -> str:
|
|
77
|
+
"""Return the name of the bare ``@action`` (non-init, single-param) function.
|
|
78
|
+
|
|
79
|
+
Only considers names matching: Next, step, Step, *_next, *Next.
|
|
80
|
+
Falls back to ``"step"`` if none is found.
|
|
81
|
+
"""
|
|
82
|
+
tree = ast.parse(spec_path.read_text())
|
|
83
|
+
for node in ast.walk(tree):
|
|
84
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
85
|
+
continue
|
|
86
|
+
if node.name == init_name:
|
|
87
|
+
continue
|
|
88
|
+
if not _STEP_NAME_RE.match(node.name):
|
|
89
|
+
continue
|
|
90
|
+
if len(node.args.args) != 1:
|
|
91
|
+
continue
|
|
92
|
+
for decorator in node.decorator_list:
|
|
93
|
+
if isinstance(decorator, ast.Name) and decorator.id == "action":
|
|
94
|
+
return node.name
|
|
95
|
+
return "step"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_examples_config(config_path: Path) -> list[ExampleConfigRow]:
|
|
99
|
+
"""Load examples YAML config.
|
|
100
|
+
|
|
101
|
+
Expected shape:
|
|
102
|
+
|
|
103
|
+
examples:
|
|
104
|
+
- file: spec.py
|
|
105
|
+
instances: inst_a inst_b # or a YAML list
|
|
106
|
+
invariants: inv_x inv_y # or a YAML list; omit for auto-discovery
|
|
107
|
+
examples: ex_a ex_b # or a YAML list; omit for auto-discovery
|
|
108
|
+
example_run_seeds: # optional mapping example -> integer seed
|
|
109
|
+
ex_b: 123
|
|
110
|
+
example_run_max_samples: # optional mapping example -> integer
|
|
111
|
+
ex_b: 300
|
|
112
|
+
timeout: 30 # optional wall-clock cap (seconds) per run
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def _to_tokens(raw: Any, key: str, file_label: str) -> list[str]:
|
|
116
|
+
if raw is None:
|
|
117
|
+
return []
|
|
118
|
+
if isinstance(raw, str):
|
|
119
|
+
return raw.split()
|
|
120
|
+
if isinstance(raw, list) and all(isinstance(x, str) for x in raw):
|
|
121
|
+
return raw
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"{config_path}: '{key}' in '{file_label}' must be a string or list[str]"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _to_optional_int(raw: Any, key: str, file_label: str) -> int | None:
|
|
127
|
+
if raw is None:
|
|
128
|
+
return None
|
|
129
|
+
if isinstance(raw, int) and not isinstance(raw, bool):
|
|
130
|
+
return raw
|
|
131
|
+
raise ValueError(f"{config_path}: '{key}' in '{file_label}' must be an integer")
|
|
132
|
+
|
|
133
|
+
def _to_str_int_map(raw: Any, key: str, file_label: str) -> dict[str, int]:
|
|
134
|
+
if raw is None:
|
|
135
|
+
return {}
|
|
136
|
+
if not isinstance(raw, dict):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"{config_path}: '{key}' in '{file_label}' must be a mapping[str, int]"
|
|
139
|
+
)
|
|
140
|
+
out: dict[str, int] = {}
|
|
141
|
+
for k, v in raw.items():
|
|
142
|
+
if not isinstance(k, str) or not isinstance(v, int):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"{config_path}: '{key}' in '{file_label}' must be a mapping[str, int]"
|
|
145
|
+
)
|
|
146
|
+
out[k] = v
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
|
150
|
+
if not isinstance(data, dict):
|
|
151
|
+
raise ValueError(f"{config_path}: root must be a mapping")
|
|
152
|
+
examples = data.get("examples")
|
|
153
|
+
if not isinstance(examples, list) or not examples:
|
|
154
|
+
raise ValueError(f"{config_path}: 'examples' must be a non-empty list")
|
|
155
|
+
|
|
156
|
+
rows: list[ExampleConfigRow] = []
|
|
157
|
+
for entry in examples:
|
|
158
|
+
if not isinstance(entry, dict):
|
|
159
|
+
raise ValueError(f"{config_path}: each example entry must be a mapping")
|
|
160
|
+
spec_file = entry.get("file")
|
|
161
|
+
if not isinstance(spec_file, str) or not spec_file.strip():
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"{config_path}: each example must define non-empty 'file'"
|
|
164
|
+
)
|
|
165
|
+
spec_file = spec_file.strip()
|
|
166
|
+
rows.append(
|
|
167
|
+
{
|
|
168
|
+
"file": spec_file,
|
|
169
|
+
"instances": _to_tokens(entry.get("instances"), "instances", spec_file),
|
|
170
|
+
"invariants_auto": "invariants" not in entry,
|
|
171
|
+
"invariants": _to_tokens(
|
|
172
|
+
entry.get("invariants"), "invariants", spec_file
|
|
173
|
+
),
|
|
174
|
+
"examples_auto": "examples" not in entry,
|
|
175
|
+
"examples": _to_tokens(entry.get("examples"), "examples", spec_file),
|
|
176
|
+
"example_run_seeds": _to_str_int_map(
|
|
177
|
+
entry.get("example_run_seeds"),
|
|
178
|
+
"example_run_seeds",
|
|
179
|
+
spec_file,
|
|
180
|
+
),
|
|
181
|
+
"example_run_max_samples": _to_str_int_map(
|
|
182
|
+
entry.get("example_run_max_samples"),
|
|
183
|
+
"example_run_max_samples",
|
|
184
|
+
spec_file,
|
|
185
|
+
),
|
|
186
|
+
"timeout": _to_optional_int(entry.get("timeout"), "timeout", spec_file),
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return rows
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert examples from examples/examples.yaml to TLA+ and validate with SANY.
|
|
3
|
+
|
|
4
|
+
For each file listed in the YAML config, this script:
|
|
5
|
+
1. Converts the base spec to ``<build_dir>/<stem>.tla``
|
|
6
|
+
2. Converts each listed ``@instance`` to ``<build_dir>/MC_<instance>_<stem>.tla``
|
|
7
|
+
3. Downloads Apalache support modules into ``build_dir`` if they are missing
|
|
8
|
+
4. Runs SANY on every generated ``.tla`` file with ``build_dir`` on the classpath
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.request
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import cast
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
22
|
+
from scripts._spec_utils import load_examples_config # noqa: E402
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).parent.parent
|
|
25
|
+
EXAMPLES_DIR = ROOT / "examples"
|
|
26
|
+
CONFIG_PATH = EXAMPLES_DIR / "examples.yaml"
|
|
27
|
+
BUILD_DIR = ROOT / ".build"
|
|
28
|
+
TLA2TOOLS_JAR = ROOT / "tla2tools.jar"
|
|
29
|
+
SUPPORT_MODULES = {
|
|
30
|
+
"Variants.tla": "https://raw.githubusercontent.com/apalache-mc/apalache/main/src/tla/Variants.tla",
|
|
31
|
+
"Apalache.tla": "https://raw.githubusercontent.com/apalache-mc/apalache/main/src/tla/Apalache.tla",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def clear_build_dir(build_dir: Path) -> None:
|
|
36
|
+
"""Remove the generated build directory if it matches the expected location."""
|
|
37
|
+
resolved_root = ROOT.resolve()
|
|
38
|
+
resolved_build_dir = build_dir.resolve(strict=False)
|
|
39
|
+
expected_build_dir = (resolved_root / ".build").resolve(strict=False)
|
|
40
|
+
|
|
41
|
+
if resolved_build_dir != expected_build_dir:
|
|
42
|
+
raise SystemExit(
|
|
43
|
+
f"Refusing to remove unexpected build directory: {resolved_build_dir}"
|
|
44
|
+
)
|
|
45
|
+
if resolved_build_dir.parent != resolved_root:
|
|
46
|
+
raise SystemExit(
|
|
47
|
+
f"Refusing to remove build directory outside project root: {resolved_build_dir}"
|
|
48
|
+
)
|
|
49
|
+
if resolved_build_dir.name != ".build":
|
|
50
|
+
raise SystemExit(
|
|
51
|
+
f"Refusing to remove directory with unexpected name: {resolved_build_dir}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if build_dir.exists():
|
|
55
|
+
shutil.rmtree(build_dir)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def ensure_support_modules(build_dir: Path) -> None:
|
|
59
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
for filename, url in SUPPORT_MODULES.items():
|
|
61
|
+
dst = build_dir / filename
|
|
62
|
+
if dst.exists():
|
|
63
|
+
continue
|
|
64
|
+
src = ROOT / filename
|
|
65
|
+
if src.exists():
|
|
66
|
+
shutil.copy(src, dst)
|
|
67
|
+
continue
|
|
68
|
+
print(f"Downloading {filename}...", flush=True)
|
|
69
|
+
urllib.request.urlretrieve(url, dst)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_cmd(cmd: list[str], *, env: dict[str, str], cwd: Path = ROOT) -> None:
|
|
73
|
+
result = subprocess.run(cmd, cwd=cwd, env=env)
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
raise SystemExit(result.returncode)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main() -> int:
|
|
79
|
+
if not TLA2TOOLS_JAR.exists():
|
|
80
|
+
print("Error: tla2tools.jar not found", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
clear_build_dir(BUILD_DIR)
|
|
84
|
+
ensure_support_modules(BUILD_DIR)
|
|
85
|
+
|
|
86
|
+
env = {**os.environ, "PYTHONPATH": str(EXAMPLES_DIR)}
|
|
87
|
+
generated: list[Path] = []
|
|
88
|
+
|
|
89
|
+
for row in load_examples_config(CONFIG_PATH):
|
|
90
|
+
spec_file = str(row["file"]).strip()
|
|
91
|
+
if not spec_file:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
stem = Path(spec_file).stem
|
|
95
|
+
instances = cast(list[str], row["instances"])
|
|
96
|
+
base_out = BUILD_DIR / f"{stem}.tla"
|
|
97
|
+
print(f"=== Converting {spec_file} ===", flush=True)
|
|
98
|
+
run_cmd(
|
|
99
|
+
[
|
|
100
|
+
sys.executable,
|
|
101
|
+
"-m",
|
|
102
|
+
"wunderspec.cli",
|
|
103
|
+
"convert",
|
|
104
|
+
"--from",
|
|
105
|
+
f"examples/{spec_file}",
|
|
106
|
+
"--to",
|
|
107
|
+
str(base_out),
|
|
108
|
+
],
|
|
109
|
+
env=env,
|
|
110
|
+
)
|
|
111
|
+
generated.append(base_out)
|
|
112
|
+
|
|
113
|
+
for instance in instances:
|
|
114
|
+
wrapper_out = BUILD_DIR / f"MC_{instance}_{stem}.tla"
|
|
115
|
+
print(
|
|
116
|
+
f"=== Converting {spec_file} instance {instance} ===",
|
|
117
|
+
flush=True,
|
|
118
|
+
)
|
|
119
|
+
run_cmd(
|
|
120
|
+
[
|
|
121
|
+
sys.executable,
|
|
122
|
+
"-m",
|
|
123
|
+
"wunderspec.cli",
|
|
124
|
+
"convert",
|
|
125
|
+
"--from",
|
|
126
|
+
f"examples/{spec_file}",
|
|
127
|
+
"--to",
|
|
128
|
+
str(wrapper_out),
|
|
129
|
+
"--instance",
|
|
130
|
+
str(instance),
|
|
131
|
+
],
|
|
132
|
+
env=env,
|
|
133
|
+
)
|
|
134
|
+
generated.append(wrapper_out)
|
|
135
|
+
|
|
136
|
+
classpath = f"{TLA2TOOLS_JAR}{os.pathsep}{BUILD_DIR}"
|
|
137
|
+
for tla_file in generated:
|
|
138
|
+
print(f"=== Checking {tla_file.relative_to(ROOT)} ===", flush=True)
|
|
139
|
+
run_cmd(
|
|
140
|
+
["java", "-cp", classpath, "tla2sany.SANY", tla_file.name],
|
|
141
|
+
env=env,
|
|
142
|
+
cwd=BUILD_DIR,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
print("All examples and listed instances converted and passed SANY")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
raise SystemExit(main())
|