wunderspec 0.128.7__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.128.7 → 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.128.7 → wunderspec-0.129.2}/PKG-INFO +3 -3
- {wunderspec-0.128.7 → wunderspec-0.129.2}/README.md +2 -2
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/examples.yaml +3 -0
- wunderspec-0.129.2/examples/sloppy_counter.py +137 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/pyproject.toml +2 -2
- 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.128.7 → wunderspec-0.129.2}/tests/README.md +14 -14
- {wunderspec-0.128.7 → wunderspec-0.129.2}/uv.lock +301 -271
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/__init__.py +1 -1
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/apalache.py +2 -1
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/api.py +10 -2
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/cli.py +19 -1
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/tlc.py +2 -1
- {wunderspec-0.128.7 → wunderspec-0.129.2}/.devcontainer/Dockerfile +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/.devcontainer/devcontainer.json +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/.devcontainer/post-create.sh +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/.github/workflows/publish.yml +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/.gitignore +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/CONTRIBUTING.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/LICENSE +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/README.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-1024.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-256.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-512.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-dark-1024.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-dark-512.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-transparent-1024.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-lockup-horizontal-dark-1600.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-lockup-horizontal-dark-800.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-mark-transparent-1024.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-readme-header-dark-1200x480.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-readme-header-dark-1600x640.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-social-preview-1280x640.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-wordmark-transparent-900.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-avatar-github.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-icon-circle-dark.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-icon-circle-transparent.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-lockup-horizontal-dark.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-lockup-horizontal-transparent.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-mark-transparent.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-readme-header-dark.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-wordmark-transparent.svg +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/basedpyrightconfig.json +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/references/from-quint-llms.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/references/from-tla-llms.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/booleans.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/cheatsheet.html +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/comprehensions.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/decorators.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/enums.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/flow.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/integers.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/lists.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/maps.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/records.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/sets.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/state-machine.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/strings.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/temporal.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/tuples.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/unions.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-stories/bobs_log.md +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-stories/bobs_log.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-stories/img/bob.png +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/bags.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/bakery.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/bakery_walk.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/ben_or.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/channel.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/dekker.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/epfd.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/fifo.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/fpaxos.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/kv_store.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/lamport_mutex.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/ledger.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/minimmit.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/payment.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/producer_consumer.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/readers_writers.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/readers_writers_walk.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_ponzi.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_ponzi_machine.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_ponzi_machine_walk.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_wal1.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_wal2.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/sliding_puzzles.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/test_simple_ponzi_machine.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/tiny_workers.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/two_phase.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/pyrightconfig.json +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/__main__.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/_edition.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/__init__.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/action_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/list_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/map_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/record_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/serialization.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/set_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/sorts.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/temporal_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/terms.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/tuple_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/union_ast.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/dev_debug.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/doc_format.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/errors.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/__init__.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/action_exec.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/context.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/scheduler.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/explain.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/expr.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/flow.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/fuzzer.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/interpreter.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/interpreter_sampling.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/interpreter_value.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/itf_trace.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/lang.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/linter.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/machine.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/model_checker.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/permutation.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/petnames.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/pretty.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/py.typed +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/quint_convert.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/random_walk.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/source_tracking.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/submachine.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/sym_context.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/tla.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/tlc_trace.py +0 -0
- {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/trace_output.py +0 -0
- {wunderspec-0.128.7 → 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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -49,6 +49,9 @@ examples:
|
|
|
49
49
|
- file: producer_consumer.py
|
|
50
50
|
instances: small medium
|
|
51
51
|
invariants: non_negative capacity_bound type_invariant
|
|
52
|
+
- file: sloppy_counter.py
|
|
53
|
+
instances: n2_batch3_8bit n4_batch4_8bit
|
|
54
|
+
invariants: accounting local_shards_bounded approximation_bound
|
|
52
55
|
- file: minimmit.py
|
|
53
56
|
instances: n6_t1_f0
|
|
54
57
|
invariants: agreement
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multiprocessor sharded counter using one percpu_counter-style counter.
|
|
3
|
+
These counters are usually used for fast statistics counters.
|
|
4
|
+
|
|
5
|
+
Inspired by the LWN discussion of Linux's ``percpu_counter``:
|
|
6
|
+
https://lwn.net/Articles/170003/
|
|
7
|
+
|
|
8
|
+
Igor Konnov, 2026
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from wunderspec import (
|
|
12
|
+
And,
|
|
13
|
+
Expr,
|
|
14
|
+
Or,
|
|
15
|
+
Param,
|
|
16
|
+
Set,
|
|
17
|
+
StateVar,
|
|
18
|
+
Tuple,
|
|
19
|
+
Val,
|
|
20
|
+
coverage,
|
|
21
|
+
example,
|
|
22
|
+
instance,
|
|
23
|
+
invariant,
|
|
24
|
+
state,
|
|
25
|
+
)
|
|
26
|
+
from wunderspec.expr import SetExpr
|
|
27
|
+
from wunderspec.machine import Context, MachineStateBase, action
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@state
|
|
31
|
+
class SloppyCounterState(MachineStateBase):
|
|
32
|
+
N: Param[int] # the total number of CPUs
|
|
33
|
+
BATCH: Param[int] # the maximal lag before syncing
|
|
34
|
+
WORD_WIDTH: Param[int] # the number of bits in the CPU word
|
|
35
|
+
global_count: StateVar[int] # the global atomic_t counter
|
|
36
|
+
local_count: StateVar[dict[int, int]] # one local counter per CPU
|
|
37
|
+
ghost_count: StateVar[int] # the exact counter by observer
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cpus(s: SloppyCounterState) -> SetExpr:
|
|
41
|
+
return Set(Val(1), ..., s.N)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def local_total(s: SloppyCounterState) -> Expr:
|
|
45
|
+
return s.local_count.reduce(lambda acc, _cpu, count: acc + count, Val(0)) # type: ignore
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def max_slop(s: SloppyCounterState) -> Expr:
|
|
49
|
+
"""The maximum counter slop over all CPUs"""
|
|
50
|
+
return s.N * (s.BATCH - 1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@action(init=True)
|
|
54
|
+
def init(c: Context[SloppyCounterState]):
|
|
55
|
+
s = c.state
|
|
56
|
+
s.global_count = Val(0)
|
|
57
|
+
s.local_count = cpus(s).map_to(lambda _: Val(0))
|
|
58
|
+
s.ghost_count = Val(0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@action(inline=False)
|
|
62
|
+
def increment(c: Context[SloppyCounterState], cpu: Expr):
|
|
63
|
+
"""Increment the sloppy counter"""
|
|
64
|
+
s = c.state
|
|
65
|
+
# the observer keeps track of the precise value
|
|
66
|
+
s.ghost_count = (s.ghost_count + 1) % (2**s.WORD_WIDTH)
|
|
67
|
+
next_local = s.local_count[cpu] + 1
|
|
68
|
+
keep_local, propagate = c.split(next_local < s.BATCH)
|
|
69
|
+
|
|
70
|
+
with keep_local: # cheap local increment
|
|
71
|
+
s.local_count[cpu] = next_local
|
|
72
|
+
|
|
73
|
+
with propagate: # expensive atomic increase
|
|
74
|
+
s.global_count = (s.global_count + next_local) % (2**s.WORD_WIDTH)
|
|
75
|
+
s.local_count[cpu] = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@action
|
|
79
|
+
def step(c: Context[SloppyCounterState]):
|
|
80
|
+
with c.one_of(cpus(c.state), "cpu") as cpu:
|
|
81
|
+
increment(c, cpu)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@invariant
|
|
85
|
+
def accounting(s: SloppyCounterState) -> Expr:
|
|
86
|
+
return s.ghost_count == (s.global_count + local_total(s)) % (2**s.WORD_WIDTH)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@invariant
|
|
90
|
+
def local_shards_bounded(s: SloppyCounterState) -> Expr:
|
|
91
|
+
return cpus(s).forall(
|
|
92
|
+
lambda cpu: And(
|
|
93
|
+
s.local_count[cpu] >= 0,
|
|
94
|
+
s.local_count[cpu] < s.BATCH,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@invariant
|
|
100
|
+
def approximation_bound(s: SloppyCounterState) -> Expr:
|
|
101
|
+
return Or(
|
|
102
|
+
And( # no overflow of ghost_count
|
|
103
|
+
s.global_count <= s.ghost_count,
|
|
104
|
+
s.ghost_count <= s.global_count + max_slop(s),
|
|
105
|
+
),
|
|
106
|
+
s.ghost_count <= (s.global_count + max_slop(s)) % (2**s.WORD_WIDTH),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@example
|
|
111
|
+
def sloppy_read_possible(s: SloppyCounterState) -> Expr:
|
|
112
|
+
return s.global_count < s.ghost_count
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@coverage
|
|
116
|
+
def state_cov(s: SloppyCounterState) -> Expr:
|
|
117
|
+
return Tuple(s.global_count, s.local_count, s.ghost_count)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@instance
|
|
121
|
+
def n2_batch3_8bit() -> SloppyCounterState:
|
|
122
|
+
return SloppyCounterState(N=2, BATCH=3, WORD_WIDTH=8)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@instance
|
|
126
|
+
def n2_batch3_16bit() -> SloppyCounterState:
|
|
127
|
+
return SloppyCounterState(N=2, BATCH=3, WORD_WIDTH=16)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@instance
|
|
131
|
+
def n2_batch100_32bit() -> SloppyCounterState:
|
|
132
|
+
return SloppyCounterState(N=2, BATCH=100, WORD_WIDTH=32)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@instance
|
|
136
|
+
def n4_batch4_8bit() -> SloppyCounterState:
|
|
137
|
+
return SloppyCounterState(N=4, BATCH=4, WORD_WIDTH=8)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wunderspec"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.129.2"
|
|
4
4
|
description = "Protocol specifications as Python code"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Igor Konnov"},
|
|
@@ -38,7 +38,7 @@ dev = [
|
|
|
38
38
|
"markdown-pytest>=0.3.2,<0.4.0",
|
|
39
39
|
"mypy>=1.0.0,<2.0.0",
|
|
40
40
|
"pyright>=1.1.400,<2.0.0",
|
|
41
|
-
"pytest>=
|
|
41
|
+
"pytest>=8.0.0,<9.0.0",
|
|
42
42
|
"pytest-codeblocks>=0.17.0,<0.18.0",
|
|
43
43
|
"pytest-cov>=6.0.0,<7.0.0",
|
|
44
44
|
]
|
|
@@ -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
|