sonolus.py 0.3.1__tar.gz → 0.3.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 (181) hide show
  1. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/.github/workflows/publish.yaml +3 -3
  2. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/.gitignore +1 -1
  3. sonolus_py-0.3.2/.python-version +1 -0
  4. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/PKG-INFO +1 -1
  5. sonolus_py-0.3.2/docs/concepts/index.md +6 -0
  6. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/concepts/project.md +27 -13
  7. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/concepts/resources.md +108 -7
  8. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/concepts/types.md +21 -4
  9. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/pyproject.toml +6 -4
  10. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/scripts/generate.py +4 -6
  11. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/node.py +13 -5
  12. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/allocate.py +41 -4
  13. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/flow.py +24 -7
  14. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/optimize.py +2 -9
  15. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/utils.py +6 -1
  16. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/visitor.py +47 -14
  17. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/cli.py +6 -1
  18. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/engine.py +1 -1
  19. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/archetype.py +27 -17
  20. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/array.py +15 -5
  21. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/array_like.py +5 -3
  22. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/containers.py +3 -3
  23. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/debug.py +66 -8
  24. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/globals.py +17 -0
  25. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/builtin_impls.py +2 -3
  26. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/context.py +50 -0
  27. sonolus_py-0.3.2/sonolus/script/internal/simulation_context.py +131 -0
  28. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/tuple_impl.py +15 -10
  29. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/iterator.py +3 -2
  30. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/num.py +11 -2
  31. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/options.py +24 -1
  32. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/quad.py +2 -0
  33. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/record.py +22 -3
  34. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/runtime.py +383 -0
  35. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/stream.py +124 -16
  36. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/transform.py +289 -0
  37. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/values.py +9 -3
  38. sonolus_py-0.3.2/tests/script/conftest.py +210 -0
  39. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_array.py +26 -26
  40. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_array_map.py +15 -29
  41. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_array_set.py +10 -17
  42. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_assert.py +7 -7
  43. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_dict.py +4 -4
  44. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_flow.py +56 -56
  45. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_functions.py +53 -53
  46. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_helpers.py +11 -11
  47. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_interval.py +22 -22
  48. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_match.py +11 -11
  49. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_num.py +12 -12
  50. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_operator.py +55 -26
  51. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_quad.py +17 -32
  52. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_random.py +10 -27
  53. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_range.py +9 -9
  54. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_record.py +18 -18
  55. sonolus_py-0.3.2/tests/script/test_transform.py +601 -0
  56. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_tuple.py +6 -6
  57. sonolus_py-0.3.2/tests/script/test_values.py +169 -0
  58. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_var_array.py +42 -61
  59. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/test_vec.py +15 -15
  60. sonolus_py-0.3.2/uv.lock +849 -0
  61. sonolus_py-0.3.1/.python-version +0 -1
  62. sonolus_py-0.3.1/docs/concepts/index.md +0 -2
  63. sonolus_py-0.3.1/tests/script/conftest.py +0 -126
  64. sonolus_py-0.3.1/tests/script/test_transform.py +0 -301
  65. sonolus_py-0.3.1/uv.lock +0 -872
  66. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/.run/Python tests in tests.run.xml +0 -0
  67. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/LICENSE +0 -0
  68. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/README.md +0 -0
  69. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/doc_stubs/__init__.py +0 -0
  70. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/doc_stubs/builtins.pyi +0 -0
  71. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/doc_stubs/math.pyi +0 -0
  72. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/doc_stubs/num.pyi +0 -0
  73. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/doc_stubs/random.pyi +0 -0
  74. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/CNAME +0 -0
  75. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/concepts/builtins.md +0 -0
  76. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/concepts/cli.md +0 -0
  77. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/concepts/constructs.md +0 -0
  78. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/index.md +0 -0
  79. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/builtins.md +0 -0
  80. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/index.md +0 -0
  81. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/math.md +0 -0
  82. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/random.md +0 -0
  83. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.archetype.md +0 -0
  84. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.array.md +0 -0
  85. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.array_like.md +0 -0
  86. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.bucket.md +0 -0
  87. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.containers.md +0 -0
  88. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.debug.md +0 -0
  89. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.easing.md +0 -0
  90. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.effect.md +0 -0
  91. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.engine.md +0 -0
  92. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.globals.md +0 -0
  93. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.instruction.md +0 -0
  94. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.interval.md +0 -0
  95. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.iterator.md +0 -0
  96. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.level.md +0 -0
  97. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.metadata.md +0 -0
  98. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.num.md +0 -0
  99. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.options.md +0 -0
  100. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.particle.md +0 -0
  101. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.printing.md +0 -0
  102. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.project.md +0 -0
  103. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.quad.md +0 -0
  104. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.record.md +0 -0
  105. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.runtime.md +0 -0
  106. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.sprite.md +0 -0
  107. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.stream.md +0 -0
  108. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.text.md +0 -0
  109. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.timing.md +0 -0
  110. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.transform.md +0 -0
  111. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.ui.md +0 -0
  112. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.values.md +0 -0
  113. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/docs/reference/sonolus.script.vec.md +0 -0
  114. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/mkdocs.yml +1 -1
  115. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/scripts/runtimes/Engine/Tutorial/Blocks.json +0 -0
  116. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/scripts/runtimes/Functions.json +0 -0
  117. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/scripts/runtimes/Level/Play/Blocks.json +0 -0
  118. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/scripts/runtimes/Level/Preview/Blocks.json +0 -0
  119. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/scripts/runtimes/Level/Watch/Blocks.json +0 -0
  120. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/__init__.py +0 -0
  121. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/__init__.py +0 -0
  122. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/blocks.py +0 -0
  123. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/excepthook.py +0 -0
  124. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/finalize.py +0 -0
  125. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/interpret.py +0 -0
  126. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/ir.py +0 -0
  127. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/mode.py +0 -0
  128. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/ops.py +0 -0
  129. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/__init__.py +0 -0
  130. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/constant_evaluation.py +0 -0
  131. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/copy_coalesce.py +0 -0
  132. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/dead_code.py +0 -0
  133. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/dominance.py +0 -0
  134. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/inlining.py +0 -0
  135. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/liveness.py +0 -0
  136. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/passes.py +0 -0
  137. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/simplify.py +0 -0
  138. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/optimize/ssa.py +0 -0
  139. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/backend/place.py +0 -0
  140. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/__init__.py +0 -0
  141. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/collection.py +0 -0
  142. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/compile.py +0 -0
  143. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/level.py +0 -0
  144. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/node.py +0 -0
  145. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/build/project.py +0 -0
  146. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/py.typed +0 -0
  147. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/__init__.py +0 -0
  148. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/bucket.py +0 -0
  149. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/easing.py +0 -0
  150. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/effect.py +0 -0
  151. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/engine.py +0 -0
  152. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/instruction.py +0 -0
  153. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/__init__.py +0 -0
  154. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/callbacks.py +0 -0
  155. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/constant.py +0 -0
  156. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/descriptor.py +0 -0
  157. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/dict_impl.py +0 -0
  158. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/error.py +0 -0
  159. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/generic.py +0 -0
  160. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/impl.py +0 -0
  161. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/introspection.py +0 -0
  162. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/math_impls.py +0 -0
  163. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/native.py +0 -0
  164. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/random.py +0 -0
  165. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/range.py +0 -0
  166. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/transient.py +0 -0
  167. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/internal/value.py +0 -0
  168. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/interval.py +0 -0
  169. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/level.py +0 -0
  170. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/metadata.py +0 -0
  171. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/particle.py +0 -0
  172. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/pointer.py +0 -0
  173. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/printing.py +0 -0
  174. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/project.py +0 -0
  175. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/sprite.py +0 -0
  176. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/text.py +0 -0
  177. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/timing.py +0 -0
  178. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/ui.py +0 -0
  179. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/sonolus/script/vec.py +0 -0
  180. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/__init__.py +0 -0
  181. {sonolus_py-0.3.1 → sonolus_py-0.3.2}/tests/script/__init__.py +0 -0
@@ -13,16 +13,16 @@ jobs:
13
13
  - name: Install uv
14
14
  uses: astral-sh/setup-uv@v3
15
15
  with:
16
- version: 0.5.2
16
+ version: 0.7.16
17
17
  - name: Install Python
18
18
  run: |
19
- uv python install 3.12 3.13
19
+ uv python install 3.12 3.13 3.14
20
20
  - name: Install project
21
21
  run: |
22
22
  uv sync --all-extras --dev
23
23
  - name: Run tests
24
24
  run: |
25
- uv run tox
25
+ CI=true uv run tox
26
26
  - name: Build
27
27
  run: |
28
28
  uv build
@@ -7,7 +7,7 @@ wheels/
7
7
  *.egg-info
8
8
 
9
9
  # Virtual environments
10
- .venv
10
+ .venv*
11
11
 
12
12
  .idea/
13
13
 
@@ -0,0 +1 @@
1
+ 3.14.0b3t
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -0,0 +1,6 @@
1
+ # Concepts
2
+ This section provides a reference for the concepts and features of Sonolus.py.
3
+
4
+ It is not intended to serve as a tutorial or introduction, and assumes familiarity with Python and Sonolus.py.
5
+
6
+ For information on how to get started with Sonolus.py, see the [home page](../index.md).
@@ -80,9 +80,16 @@ play_mode = PlayMode(
80
80
 
81
81
  ```
82
82
 
83
- Play mode archetypes subclass [`PlayArchetype`][sonolus.script.archetype.PlayArchetype] and should implement the [`should_spawn`][sonolus.script.archetype.PlayArchetype.should_spawn] callback. They may also implement
84
- the [`preprocess`][sonolus.script.archetype.PlayArchetype.preprocess], [`spawn_order`][sonolus.script.archetype.PlayArchetype.spawn_order], [`initialize`][sonolus.script.archetype.PlayArchetype.initialize], [`update_sequential`][sonolus.script.archetype.PlayArchetype.update_sequential], [`update_parallel`][sonolus.script.archetype.PlayArchetype.update_parallel], [`touch`][sonolus.script.archetype.PlayArchetype.touch], and
85
- [`terminate`][sonolus.script.archetype.PlayArchetype.terminate] callbacks.
83
+ Play mode archetypes subclass [`PlayArchetype`][sonolus.script.archetype.PlayArchetype] and implement the following callbacks:
84
+
85
+ - [`should_spawn`][sonolus.script.archetype.PlayArchetype.should_spawn] (required)
86
+ - [`preprocess`][sonolus.script.archetype.PlayArchetype.preprocess]
87
+ - [`spawn_order`][sonolus.script.archetype.PlayArchetype.spawn_order]
88
+ - [`initialize`][sonolus.script.archetype.PlayArchetype.initialize]
89
+ - [`update_sequential`][sonolus.script.archetype.PlayArchetype.update_sequential]
90
+ - [`update_parallel`][sonolus.script.archetype.PlayArchetype.update_parallel]
91
+ - [`touch`][sonolus.script.archetype.PlayArchetype.touch]
92
+ - [`terminate`][sonolus.script.archetype.PlayArchetype.terminate]
86
93
 
87
94
  Archetypes for scored notes should have the [`is_scored`][sonolus.script.archetype.PlayArchetype.is_scored] class variable set to `True`.
88
95
 
@@ -110,9 +117,15 @@ watch_mode = WatchMode(
110
117
  )
111
118
  ```
112
119
 
113
- Watch mode archetypes subclass [`WatchArchetype`][sonolus.script.archetype.WatchArchetype] and should implement the [`spawn_time`][sonolus.script.archetype.WatchArchetype.spawn_time] and [`despawn_time`][sonolus.script.archetype.WatchArchetype.despawn_time] callbacks.
114
- They may also implement the [`preprocess`][sonolus.script.archetype.WatchArchetype.preprocess], [`initialize`][sonolus.script.archetype.WatchArchetype.initialize], [`update_sequential`][sonolus.script.archetype.WatchArchetype.update_sequential], [`update_parallel`][sonolus.script.archetype.WatchArchetype.update_parallel], and
115
- [`terminate`][sonolus.script.archetype.WatchArchetype.terminate] callbacks.
120
+ Watch mode archetypes subclass [`WatchArchetype`][sonolus.script.archetype.WatchArchetype] and implement the following callbacks:
121
+
122
+ - [`spawn_time`][sonolus.script.archetype.WatchArchetype.spawn_time] (required)
123
+ - [`despawn_time`][sonolus.script.archetype.WatchArchetype.despawn_time] (required)
124
+ - [`preprocess`][sonolus.script.archetype.WatchArchetype.preprocess]
125
+ - [`initialize`][sonolus.script.archetype.WatchArchetype.initialize]
126
+ - [`update_sequential`][sonolus.script.archetype.WatchArchetype.update_sequential]
127
+ - [`update_parallel`][sonolus.script.archetype.WatchArchetype.update_parallel]
128
+ - [`terminate`][sonolus.script.archetype.WatchArchetype.terminate]
116
129
 
117
130
  Watch mode also has the `update_spawn` global callback, which is invoked every frame and should return the reference
118
131
  time to compare against spawn and despawn times of archetypes. Typically, this can be either the current time or the
@@ -135,7 +148,10 @@ preview_mode = PreviewMode(
135
148
  )
136
149
  ```
137
150
 
138
- Preview mode archetypes subclass [`PreviewArchetype`][sonolus.script.archetype.PreviewArchetype] and may implement the [`preprocess`][sonolus.script.archetype.PreviewArchetype.preprocess] and [`render`][sonolus.script.archetype.PreviewArchetype.render] callbacks.
151
+ Preview mode archetypes subclass [`PreviewArchetype`][sonolus.script.archetype.PreviewArchetype] and implement the following callbacks:
152
+
153
+ - [`preprocess`][sonolus.script.archetype.PreviewArchetype.preprocess]
154
+ - [`render`][sonolus.script.archetype.PreviewArchetype.render]
139
155
 
140
156
  ### Tutorial Mode
141
157
 
@@ -162,13 +178,11 @@ tutorial_mode = TutorialMode(
162
178
  )
163
179
  ```
164
180
 
165
- Tutorial mode does not have archetypes, but it has `preprocess`, `navigate`, and `update` global callbacks.
166
-
167
- `preprocess` is invoked once before the tutorial starts.
168
-
169
- `navigate` is invoked when the player navigates forward or backward in the tutorial.
181
+ Tutorial mode does not have archetypes, but has the following global callbacks:
170
182
 
171
- `update` is invoked every frame and should handle most of the drawing logic.
183
+ - `preprocess` - Invoked once before the tutorial starts
184
+ - `navigate` - Invoked when the player navigates forward or backward in the tutorial
185
+ - `update` - Invoked every frame and should handle most of the drawing logic
172
186
 
173
187
  ## Levels
174
188
  Levels are defined using the [`Level`][sonolus.script.level.Level] class:
@@ -1,32 +1,133 @@
1
1
  # Resources & Declarations
2
2
 
3
- ## Level Memory & Level Data
4
- Level memory and level data are defined with the [`@level_memory`][sonolus.script.globals.level_memory] and [`@level_data`][sonolus.script.globals.level_data] class decorators, respectively:
3
+ ## Global Variables
4
+
5
+ ### Level Memory
6
+ Level memory is defined with the [`@level_memory`][sonolus.script.globals.level_memory] class decorator:
5
7
 
6
8
  ```python
7
- from sonolus.script.globals import level_memory, level_data
9
+ from sonolus.script.globals import level_memory
8
10
 
9
11
 
10
12
  @level_memory
11
13
  class LevelMemory:
12
14
  value: int
13
-
15
+ ```
16
+
17
+ Alternatively, it may be called as a function as well by passing the type as an argument:
18
+
19
+ ```python
20
+ from sonolus.script.globals import level_memory
21
+ from sonolus.script.vec import Vec2
22
+
23
+
24
+ level_memory_value = level_memory(Vec2)
25
+ ```
26
+
27
+ Level memory may be modified in sequential callbacks:
28
+
29
+ - `preprocess`
30
+ - `update_sequential`
31
+ - `touch`
32
+
33
+ and may be read in any callback.
34
+
35
+ ### Level Data
36
+ Level data is defined with the [`@level_data`][sonolus.script.globals.level_data] class decorator:
37
+
38
+ ```python
39
+ from sonolus.script.globals import level_data
40
+
14
41
 
15
42
  @level_data
16
43
  class LevelData:
17
44
  value: int
18
45
  ```
19
46
 
20
- Alternatively, they may be called as functions as well by passing the type as an argument:
47
+ Alternatively, it may be called as a function as well by passing the type as an argument:
48
+
21
49
  ```python
22
- from sonolus.script.globals import level_memory, level_data
50
+ from sonolus.script.globals import level_data
23
51
  from sonolus.script.vec import Vec2
24
52
 
25
53
 
26
- level_memory_value = level_memory(Vec2)
27
54
  level_data_value = level_data(Vec2)
28
55
  ```
29
56
 
57
+ Level data may only be modified in the `preprocess` callback and may be read in any callback.
58
+
59
+ ## Archetype Variables
60
+
61
+ ### Imported
62
+ Imported fields are declared with [`imported()`][sonolus.script.archetype.imported]:
63
+
64
+ ```python
65
+ from sonolus.script.archetype import PlayArchetype, imported
66
+
67
+ class MyArchetype(PlayArchetype):
68
+ field: int = imported()
69
+ field_with_explicit_name: int = imported(name="field_name")
70
+ ```
71
+
72
+ Imported fields may be loaded from the level data. In watch mode, data may also be loaded from a corresponding exported field in play mode.
73
+
74
+ Imported fields may only be updated in the `preprocess` callback, and are read-only in other callbacks.
75
+
76
+ ### Exported
77
+ Exported fields are declared with [`exported()`][sonolus.script.archetype.exported]:
78
+
79
+ ```python
80
+ from sonolus.script.archetype import PlayArchetype, exported
81
+
82
+ class MyArchetype(PlayArchetype):
83
+ field: int = exported()
84
+ field_with_explicit_name: int = exported(name="#FIELD")
85
+ ```
86
+
87
+ This is only usable in play mode to export data to be loaded in watch mode. Exported fields are write-only.
88
+
89
+ ### Entity Data
90
+ Entity data fields are declared with [`entity_data()`][sonolus.script.archetype.entity_data]:
91
+
92
+ ```python
93
+ from sonolus.script.archetype import PlayArchetype, entity_data
94
+
95
+ class MyArchetype(PlayArchetype):
96
+ field: int = entity_data()
97
+ ```
98
+
99
+ Entity data is accessible from other entities, but may only be updated in the `preprocess` callback and is read-only in other callbacks.
100
+
101
+ It functions like [`imported()`][sonolus.script.archetype.imported] and shares the same underlying storage, except that it is not loaded from a level.
102
+
103
+ ### Entity Memory
104
+ Entity memory fields are declared with [`entity_memory()`][sonolus.script.archetype.entity_memory]:
105
+
106
+ ```python
107
+ from sonolus.script.archetype import PlayArchetype, entity_memory
108
+
109
+ class MyArchetype(PlayArchetype):
110
+ field: int = entity_memory()
111
+ ```
112
+
113
+ Entity memory is private to the entity and is not accessible from other entities. It may be read or updated in any callback associated with the entity.
114
+
115
+ Entity memory fields may also be set when an entity is spawned using the [`spawn()`][sonolus.script.archetype.PlayArchetype.spawn] method.
116
+
117
+ ### Shared Memory
118
+ Shared memory fields are declared with [`shared_memory()`][sonolus.script.archetype.shared_memory]:
119
+
120
+ ```python
121
+ from sonolus.script.archetype import PlayArchetype, shared_memory
122
+
123
+ class MyArchetype(PlayArchetype):
124
+ field: int = shared_memory()
125
+ ```
126
+
127
+ Shared memory is accessible from other entities.
128
+
129
+ Shared memory may be read in any callback, but may only be updated by sequential callbacks (`preprocess`, `update_sequential`, and `touch`).
130
+
30
131
  ## Streams
31
132
  Streams are defined with the [`@streams`][sonolus.script.stream.streams] decorator:
32
133
 
@@ -1,5 +1,3 @@
1
- from sonolus.script.archetype import PlayArchetype
2
-
3
1
  # Types
4
2
  Sonolus.py has 3 core types: [`Num`](#num), [`Array`](#array), and [`Record`](#record). representing numeric values, fixed-size arrays,
5
3
  and custom data structures, respectively. Arrays and records can be nested within each other to create complex data
@@ -106,11 +104,12 @@ from sonolus.script.array import Array
106
104
 
107
105
  ### Declaration
108
106
 
109
- Arrays can be created using its constructor:
107
+ Arrays can be created using its constructor or the unary `+` operator.
110
108
 
111
109
  ```python
112
110
  a1 = Array[int, 3](1, 2, 3)
113
111
  a2 = Array[int, 0]()
112
+ a3 = +Array[int, 3] # Create a zero-initialized array
114
113
  ```
115
114
 
116
115
  If at least one element is provided, the element type and size can be inferred:
@@ -152,6 +151,14 @@ assert a[0] == Pair(1, 2) # The value in the array is independent of the origin
152
151
 
153
152
  ### Operations
154
153
 
154
+ An array can be copied with the unary `+` operator, which creates a new array with the same elements:
155
+
156
+ ```python
157
+ a = Array(1, 2, 3)
158
+ b = +a
159
+ assert b == Array(1, 2, 3)
160
+ ```
161
+
155
162
  The value of an array can be copied from another array using the copy from operator (`@=`)[^1]:
156
163
 
157
164
  ```python
@@ -299,11 +306,13 @@ class MyPairSubclass(MyPair):
299
306
 
300
307
  ### Instantiation
301
308
 
302
- A constructor is automatically generated for the [`Record`][sonolus.script.record.Record] class:
309
+ A constructor is automatically generated for the [`Record`][sonolus.script.record.Record] class and the unary `+`
310
+ operator can also be used to create a zero-initialized record.
303
311
 
304
312
  ```python
305
313
  pair_1 = MyPair(1, 2)
306
314
  pair_2 = MyPair(first=1, second=2)
315
+ pair_3 = +MyPair # Create a zero-initialized record
307
316
  ```
308
317
 
309
318
  ### Generics
@@ -342,6 +351,14 @@ assert MyGenericRecord(1).my_type() == Num
342
351
 
343
352
  ### Operations
344
353
 
354
+ A record can be copied with the unary `+` operator, which creates a new record with the same field values:
355
+
356
+ ```python
357
+ pair = MyPair(1, 2)
358
+ copy_pair = +pair
359
+ assert copy_pair == MyPair(1, 2)
360
+ ```
361
+
345
362
  The value of a record can be copied from another record using the copy from operator (`@=`)[^1]:
346
363
 
347
364
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sonolus.py"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Sonolus engine development in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -13,12 +13,12 @@ default-groups = ["dev", "docs"]
13
13
 
14
14
  [tool.ruff]
15
15
  line-length = 120
16
- target-version = "py313"
16
+ target-version = "py312"
17
17
 
18
18
  [tool.ruff.lint]
19
19
  preview = true
20
20
  select = ["F", "E", "W", "I", "N", "D", "UP", "YTT", "B", "A", "COM", "C4", "DTZ", "PIE", "PT", "Q", "SLOT", "SIM", "PTH", "PL", "PERF", "FURB", "LOG", "RUF"]
21
- ignore = ["E402", "D1", "COM812", "PLW2901", "PLW3201", "PLR6301", "PLC0415", "PLR2004", "PLR09", "SIM108", "FURB113", "A005"]
21
+ ignore = ["E402", "D1", "COM812", "PLW2901", "PLW3201", "PLR6301", "PLC0415", "PLR2004", "PLR09", "SIM108", "FURB113", "A005", "B903"]
22
22
 
23
23
  [tool.ruff.lint.pydocstyle]
24
24
  convention = "google"
@@ -49,13 +49,15 @@ packages = ["sonolus"]
49
49
 
50
50
  [tool.tox]
51
51
  requires = ["tox>=4.19"]
52
- env_list = ["py312", "py313"]
52
+ env_list = ["py312", "py313", "py314"]
53
53
 
54
54
  [tool.tox.env_run_base]
55
55
  description = "Run tests"
56
+ passenv = ["CI"]
56
57
  deps = [
57
58
  "hypothesis>=6.115.3",
58
59
  "pytest-xdist>=3.6.1",
59
60
  "pytest>=8.3.3",
60
61
  ]
62
+ uv_python_preference = "managed"
61
63
  commands = [["pytest", "tests", "-n", "auto"]]
@@ -51,11 +51,9 @@ def block(name: str, f: TextIO, out: TextIO):
51
51
  readable = block["readable"]
52
52
  writable = block["writable"]
53
53
  out.write(
54
- f' {name} = ({id_}, {{{
55
- ", ".join(f'"{e}"' for e in readable)
56
- }}}, {{{
57
- ", ".join(f'"{e}"' for e in writable)
58
- }}})\n'
54
+ f" {name} = ({id_}, {{{', '.join(f'"{e}"' for e in readable)}}}, {{{
55
+ ', '.join(f'"{e}"' for e in writable)
56
+ }}})\n"
59
57
  )
60
58
 
61
59
 
@@ -78,7 +76,7 @@ def blocks():
78
76
  (runtimes_dir / file).open("r", encoding="utf-8") as f,
79
77
  ):
80
78
  block(name, f, out)
81
- out.write(f"\n" f"\n" f"type Block = {" | ".join(f'{name}Block' for name in block_files)}\n")
79
+ out.write(f"\n\ntype Block = {' | '.join(f'{name}Block' for name in block_files)}\n")
82
80
 
83
81
 
84
82
  def main():
@@ -1,26 +1,34 @@
1
1
  import textwrap
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
 
4
4
  from sonolus.backend.ops import Op
5
5
 
6
6
  type EngineNode = ConstantNode | FunctionNode
7
7
 
8
8
 
9
- @dataclass
9
+ @dataclass(slots=True)
10
10
  class ConstantNode:
11
11
  value: float
12
+ _hash: int = field(init=False, repr=False)
13
+
14
+ def __post_init__(self):
15
+ self._hash = hash(self.value)
12
16
 
13
17
  def __hash__(self):
14
18
  return hash(self.value)
15
19
 
16
20
 
17
- @dataclass
21
+ @dataclass(slots=True)
18
22
  class FunctionNode:
19
23
  func: Op
20
24
  args: list[EngineNode]
25
+ _hash: int = field(init=False, repr=False)
26
+
27
+ def __post_init__(self):
28
+ self._hash = hash((self.func, tuple(self.args)))
21
29
 
22
30
  def __hash__(self):
23
- return hash((self.func, tuple(self.args)))
31
+ return self._hash
24
32
 
25
33
 
26
34
  def format_engine_node(node: EngineNode) -> str:
@@ -34,7 +42,7 @@ def format_engine_node(node: EngineNode) -> str:
34
42
  return f"{node.func.name}({format_engine_node(node.args[0])})"
35
43
  case _:
36
44
  return f"{node.func.name}(\n{
37
- textwrap.indent("\n".join(format_engine_node(arg) for arg in node.args), " ")
45
+ textwrap.indent('\n'.join(format_engine_node(arg) for arg in node.args), ' ')
38
46
  }\n)"
39
47
  else:
40
48
  raise ValueError(f"Invalid engine node: {node}")
@@ -65,7 +65,8 @@ class Allocate(CompilerPass):
65
65
  def run(self, entry: BasicBlock):
66
66
  mapping = self.get_mapping(entry)
67
67
  for block in traverse_cfg_preorder(entry):
68
- block.statements = [self.update_stmt(statement, mapping) for statement in block.statements]
68
+ updated_statements = [self.update_stmt(statement, mapping) for statement in block.statements]
69
+ block.statements = [stmt for stmt in updated_statements if stmt is not None]
69
70
  block.test = self.update_stmt(block.test, mapping)
70
71
  return entry
71
72
 
@@ -80,7 +81,19 @@ class Allocate(CompilerPass):
80
81
  case IRGet(place=place):
81
82
  return IRGet(place=self.update_stmt(place, mapping))
82
83
  case IRSet(place=place, value=value):
83
- return IRSet(place=self.update_stmt(place, mapping), value=self.update_stmt(value, mapping))
84
+ # Do some dead code elimination here which is pretty much free since we already have liveness analysis,
85
+ # and prevents an error from the dead block place being missing from the mapping.
86
+ live = get_live(stmt)
87
+ is_live = not (
88
+ (isinstance(place, BlockPlace) and isinstance(place.block, TempBlock) and place.block not in live)
89
+ or (isinstance(value, IRGet) and place == value.place)
90
+ )
91
+ if is_live:
92
+ return IRSet(place=self.update_stmt(place, mapping), value=self.update_stmt(value, mapping))
93
+ elif isinstance(value, IRInstr) and value.op.side_effects:
94
+ return self.update_stmt(value, mapping)
95
+ else:
96
+ return None
84
97
  case BlockPlace(block=block, index=index, offset=offset):
85
98
  if isinstance(block, TempBlock):
86
99
  if block.size == 0:
@@ -119,8 +132,32 @@ class Allocate(CompilerPass):
119
132
  def get_interference(self, entry: BasicBlock) -> dict[TempBlock, set[TempBlock]]:
120
133
  result = {}
121
134
  for block in traverse_cfg_preorder(entry):
122
- for stmt in [*block.statements, block.test]:
135
+ for stmt in block.statements:
136
+ if not isinstance(stmt, IRSet):
137
+ continue
123
138
  live = {p for p in get_live(stmt) if isinstance(p, TempBlock) and p.size > 0}
124
139
  for place in live:
125
- result.setdefault(place, set()).update(live - {place})
140
+ if place not in result:
141
+ result[place] = set(live)
142
+ else:
143
+ result[place].update(live)
126
144
  return result
145
+
146
+
147
+ class AllocateFast(Allocate):
148
+ """A bit faster than Allocate but a bit less optimal."""
149
+
150
+ def get_mapping(self, entry: BasicBlock) -> dict[TempBlock, int]:
151
+ interference = self.get_interference(entry)
152
+ offsets: dict[TempBlock, int] = dict.fromkeys(interference, 0)
153
+ end_offsets: dict[TempBlock, int] = dict.fromkeys(interference, 0)
154
+
155
+ for block, others in interference.items():
156
+ size = block.size
157
+ offset = max((end_offsets[other] for other in others), default=0)
158
+ if offset + size > TEMP_SIZE:
159
+ raise ValueError("Temporary memory limit exceeded")
160
+ offsets[block] = offset
161
+ end_offsets[block] = offset + size
162
+
163
+ return offsets
@@ -91,12 +91,29 @@ def cfg_to_mermaid(entry: BasicBlock):
91
91
 
92
92
  lines = ["Entry([Entry]) --> 0"]
93
93
  for block, index in block_indexes.items():
94
- lines.append(f"{index}[{pre(fmt([f'#{index}', *(
95
- f"{dst} := phi({", ".join(f"{block_indexes.get(src_block, "<dead>")}: {src_place}"
96
- for src_block, src_place
97
- in sorted(phis.items(), key=lambda x: block_indexes.get(x[0])))})"
98
- for dst, phis in block.phis.items()
99
- ), *block.statements]))}]")
94
+ lines.append(
95
+ f"{index}[{
96
+ pre(
97
+ fmt(
98
+ [
99
+ f'#{index}',
100
+ *(
101
+ f'{dst} := phi({
102
+ ", ".join(
103
+ f"{block_indexes.get(src_block, '<dead>')}: {src_place}"
104
+ for src_block, src_place in sorted(
105
+ phis.items(), key=lambda x: block_indexes.get(x[0])
106
+ )
107
+ )
108
+ })'
109
+ for dst, phis in block.phis.items()
110
+ ),
111
+ *block.statements,
112
+ ]
113
+ )
114
+ )
115
+ }]"
116
+ )
100
117
 
101
118
  outgoing = {edge.cond: edge.dst for edge in block.outgoing}
102
119
  match outgoing:
@@ -114,7 +131,7 @@ def cfg_to_mermaid(entry: BasicBlock):
114
131
  lines.append(f"{index} --> {index}_")
115
132
  for cond, target in tgt.items():
116
133
  lines.append(
117
- f"{index}_ --> |{pre(fmt([cond if cond is not None else "default"]))}| {block_indexes[target]}"
134
+ f"{index}_ --> |{pre(fmt([cond if cond is not None else 'default']))}| {block_indexes[target]}"
118
135
  )
119
136
  lines.append("Exit([Exit])")
120
137
 
@@ -1,4 +1,4 @@
1
- from sonolus.backend.optimize.allocate import Allocate, AllocateBasic
1
+ from sonolus.backend.optimize.allocate import Allocate, AllocateBasic, AllocateFast
2
2
  from sonolus.backend.optimize.constant_evaluation import SparseConditionalConstantPropagation
3
3
  from sonolus.backend.optimize.copy_coalesce import CopyCoalesce
4
4
  from sonolus.backend.optimize.dead_code import (
@@ -6,9 +6,7 @@ from sonolus.backend.optimize.dead_code import (
6
6
  DeadCodeElimination,
7
7
  UnreachableCodeElimination,
8
8
  )
9
- from sonolus.backend.optimize.flow import BasicBlock
10
9
  from sonolus.backend.optimize.inlining import InlineVars
11
- from sonolus.backend.optimize.passes import run_passes
12
10
  from sonolus.backend.optimize.simplify import CoalesceFlow, NormalizeSwitch, RewriteToSwitch
13
11
  from sonolus.backend.optimize.ssa import FromSSA, ToSSA
14
12
 
@@ -21,9 +19,8 @@ MINIMAL_PASSES = (
21
19
  FAST_PASSES = (
22
20
  CoalesceFlow(),
23
21
  UnreachableCodeElimination(),
24
- AdvancedDeadCodeElimination(),
22
+ AllocateFast(), # Does dead code elimination too, so no need for a separate pass
25
23
  CoalesceFlow(),
26
- Allocate(),
27
24
  )
28
25
 
29
26
  STANDARD_PASSES = (
@@ -46,7 +43,3 @@ STANDARD_PASSES = (
46
43
  NormalizeSwitch(),
47
44
  Allocate(),
48
45
  )
49
-
50
-
51
- def optimize_and_allocate(cfg: BasicBlock):
52
- return run_passes(cfg, STANDARD_PASSES)
@@ -11,10 +11,15 @@ def get_function(fn: Callable) -> tuple[str, ast.FunctionDef]:
11
11
  # This preserves both line number and column number in the returned node
12
12
  source_file = inspect.getsourcefile(fn)
13
13
  _, start_line = inspect.getsourcelines(fn)
14
- base_tree = ast.parse(Path(source_file).read_text(encoding="utf-8"))
14
+ base_tree = get_tree_from_file(source_file)
15
15
  return source_file, find_function(base_tree, start_line)
16
16
 
17
17
 
18
+ @cache
19
+ def get_tree_from_file(file: str | Path) -> ast.Module:
20
+ return ast.parse(Path(file).read_text(encoding="utf-8"))
21
+
22
+
18
23
  class FindFunction(ast.NodeVisitor):
19
24
  def __init__(self, line):
20
25
  self.line = line