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