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.
Files changed (146) hide show
  1. {wunderspec-0.128.7 → wunderspec-0.129.2}/.flake8 +1 -0
  2. wunderspec-0.129.2/.github/workflows/build.yml +34 -0
  3. wunderspec-0.129.2/.github/workflows/convert-to-tla.yml +78 -0
  4. wunderspec-0.129.2/.github/workflows/lint.yml +40 -0
  5. wunderspec-0.129.2/.github/workflows/run-examples.yml +31 -0
  6. {wunderspec-0.128.7 → wunderspec-0.129.2}/PKG-INFO +3 -3
  7. {wunderspec-0.128.7 → wunderspec-0.129.2}/README.md +2 -2
  8. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/examples.yaml +3 -0
  9. wunderspec-0.129.2/examples/sloppy_counter.py +137 -0
  10. {wunderspec-0.128.7 → wunderspec-0.129.2}/pyproject.toml +2 -2
  11. wunderspec-0.129.2/scripts/__init__.py +1 -0
  12. wunderspec-0.129.2/scripts/_spec_utils.py +190 -0
  13. wunderspec-0.129.2/scripts/convert_examples_to_tla.py +150 -0
  14. wunderspec-0.129.2/scripts/run_examples.py +163 -0
  15. {wunderspec-0.128.7 → wunderspec-0.129.2}/tests/README.md +14 -14
  16. {wunderspec-0.128.7 → wunderspec-0.129.2}/uv.lock +301 -271
  17. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/__init__.py +1 -1
  18. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/apalache.py +2 -1
  19. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/api.py +10 -2
  20. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/cli.py +19 -1
  21. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/tlc.py +2 -1
  22. {wunderspec-0.128.7 → wunderspec-0.129.2}/.devcontainer/Dockerfile +0 -0
  23. {wunderspec-0.128.7 → wunderspec-0.129.2}/.devcontainer/devcontainer.json +0 -0
  24. {wunderspec-0.128.7 → wunderspec-0.129.2}/.devcontainer/post-create.sh +0 -0
  25. {wunderspec-0.128.7 → wunderspec-0.129.2}/.github/workflows/publish.yml +0 -0
  26. {wunderspec-0.128.7 → wunderspec-0.129.2}/.gitignore +0 -0
  27. {wunderspec-0.128.7 → wunderspec-0.129.2}/CONTRIBUTING.md +0 -0
  28. {wunderspec-0.128.7 → wunderspec-0.129.2}/LICENSE +0 -0
  29. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/README.md +0 -0
  30. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-1024.png +0 -0
  31. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-256.png +0 -0
  32. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-avatar-github-512.png +0 -0
  33. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-dark-1024.png +0 -0
  34. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-dark-512.png +0 -0
  35. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-icon-circle-transparent-1024.png +0 -0
  36. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-lockup-horizontal-dark-1600.png +0 -0
  37. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-lockup-horizontal-dark-800.png +0 -0
  38. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-mark-transparent-1024.png +0 -0
  39. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-readme-header-dark-1200x480.png +0 -0
  40. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-readme-header-dark-1600x640.png +0 -0
  41. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-social-preview-1280x640.png +0 -0
  42. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/png/wunderspec-wordmark-transparent-900.png +0 -0
  43. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-avatar-github.svg +0 -0
  44. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-icon-circle-dark.svg +0 -0
  45. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-icon-circle-transparent.svg +0 -0
  46. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-lockup-horizontal-dark.svg +0 -0
  47. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-lockup-horizontal-transparent.svg +0 -0
  48. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-mark-transparent.svg +0 -0
  49. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-readme-header-dark.svg +0 -0
  50. {wunderspec-0.128.7 → wunderspec-0.129.2}/assets/design/svg/wunderspec-wordmark-transparent.svg +0 -0
  51. {wunderspec-0.128.7 → wunderspec-0.129.2}/basedpyrightconfig.json +0 -0
  52. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/references/from-quint-llms.md +0 -0
  53. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/references/from-tla-llms.md +0 -0
  54. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/booleans.md +0 -0
  55. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/cheatsheet.html +0 -0
  56. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/comprehensions.md +0 -0
  57. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/decorators.md +0 -0
  58. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/enums.md +0 -0
  59. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/flow.md +0 -0
  60. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/integers.md +0 -0
  61. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/lists.md +0 -0
  62. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/maps.md +0 -0
  63. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/records.md +0 -0
  64. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/sets.md +0 -0
  65. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/state-machine.md +0 -0
  66. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/strings.md +0 -0
  67. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/temporal.md +0 -0
  68. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/tuples.md +0 -0
  69. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-references/unions.md +0 -0
  70. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-stories/bobs_log.md +0 -0
  71. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-stories/bobs_log.png +0 -0
  72. {wunderspec-0.128.7 → wunderspec-0.129.2}/docs/user-stories/img/bob.png +0 -0
  73. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/bags.py +0 -0
  74. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/bakery.py +0 -0
  75. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/bakery_walk.py +0 -0
  76. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/ben_or.py +0 -0
  77. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/channel.py +0 -0
  78. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/dekker.py +0 -0
  79. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/epfd.py +0 -0
  80. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/fifo.py +0 -0
  81. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/fpaxos.py +0 -0
  82. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/kv_store.py +0 -0
  83. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/lamport_mutex.py +0 -0
  84. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/ledger.py +0 -0
  85. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/minimmit.py +0 -0
  86. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/payment.py +0 -0
  87. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/producer_consumer.py +0 -0
  88. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/readers_writers.py +0 -0
  89. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/readers_writers_walk.py +0 -0
  90. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_ponzi.py +0 -0
  91. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_ponzi_machine.py +0 -0
  92. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_ponzi_machine_walk.py +0 -0
  93. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_wal1.py +0 -0
  94. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/simple_wal2.py +0 -0
  95. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/sliding_puzzles.py +0 -0
  96. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/test_simple_ponzi_machine.py +0 -0
  97. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/tiny_workers.py +0 -0
  98. {wunderspec-0.128.7 → wunderspec-0.129.2}/examples/two_phase.py +0 -0
  99. {wunderspec-0.128.7 → wunderspec-0.129.2}/pyrightconfig.json +0 -0
  100. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/__main__.py +0 -0
  101. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/_edition.py +0 -0
  102. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/__init__.py +0 -0
  103. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/action_ast.py +0 -0
  104. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/ast.py +0 -0
  105. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/list_ast.py +0 -0
  106. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/map_ast.py +0 -0
  107. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/record_ast.py +0 -0
  108. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/serialization.py +0 -0
  109. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/set_ast.py +0 -0
  110. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/sorts.py +0 -0
  111. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/temporal_ast.py +0 -0
  112. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/terms.py +0 -0
  113. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/tuple_ast.py +0 -0
  114. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/ast/union_ast.py +0 -0
  115. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/dev_debug.py +0 -0
  116. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/doc_format.py +0 -0
  117. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/errors.py +0 -0
  118. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/__init__.py +0 -0
  119. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/action_exec.py +0 -0
  120. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/context.py +0 -0
  121. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/exec/scheduler.py +0 -0
  122. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/explain.py +0 -0
  123. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/expr.py +0 -0
  124. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/flow.py +0 -0
  125. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/fuzzer.py +0 -0
  126. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/interpreter.py +0 -0
  127. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/interpreter_sampling.py +0 -0
  128. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/interpreter_value.py +0 -0
  129. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/itf_trace.py +0 -0
  130. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/lang.py +0 -0
  131. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/linter.py +0 -0
  132. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/machine.py +0 -0
  133. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/model_checker.py +0 -0
  134. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/permutation.py +0 -0
  135. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/petnames.py +0 -0
  136. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/pretty.py +0 -0
  137. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/py.typed +0 -0
  138. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/quint_convert.py +0 -0
  139. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/random_walk.py +0 -0
  140. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/source_tracking.py +0 -0
  141. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/submachine.py +0 -0
  142. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/sym_context.py +0 -0
  143. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/tla.py +0 -0
  144. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/tlc_trace.py +0 -0
  145. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/trace_output.py +0 -0
  146. {wunderspec-0.128.7 → wunderspec-0.129.2}/wunderspec/uniq_names.py +0 -0
@@ -2,5 +2,6 @@
2
2
  max-line-length = 88
3
3
  extend-ignore = E203, E501
4
4
  per-file-ignores =
5
+ examples/*:E266,F403,F405
5
6
  tests/*:F401,F841
6
7
  wunderspec/lang.py:E721
@@ -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.128.7
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.128.7`
78
- - Source commit: `ad5ef3606668d6720f41fc18dbb1cd86c682cbd7`
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.128.7`
62
- - Source commit: `ad5ef3606668d6720f41fc18dbb1cd86c682cbd7`
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.128.7"
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>=9.0.2,<10.0.0",
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