sonolus.py 0.3.2__tar.gz → 0.3.3__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.

Potentially problematic release.


This version of sonolus.py might be problematic. Click here for more details.

Files changed (177) hide show
  1. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/PKG-INFO +1 -1
  2. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/builtins.md +5 -1
  3. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/types.md +5 -3
  4. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/pyproject.toml +1 -1
  5. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/finalize.py +16 -4
  6. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/visitor.py +25 -9
  7. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/archetype.py +25 -7
  8. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/array.py +5 -3
  9. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/array_like.py +25 -0
  10. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/containers.py +24 -4
  11. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/builtin_impls.py +10 -5
  12. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/context.py +5 -1
  13. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/range.py +25 -2
  14. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/tuple_impl.py +3 -1
  15. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/interval.py +60 -2
  16. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/quad.py +39 -3
  17. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/record.py +2 -0
  18. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/runtime.py +28 -0
  19. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/stream.py +22 -13
  20. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/transform.py +4 -4
  21. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/vec.py +14 -2
  22. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_array.py +56 -0
  23. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_array_map.py +96 -0
  24. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_array_set.py +17 -0
  25. sonolus_py-0.3.3/tests/script/test_interval.py +570 -0
  26. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_operator.py +104 -0
  27. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_range.py +73 -0
  28. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_tuple.py +70 -0
  29. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_var_array.py +98 -7
  30. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/uv.lock +1 -1
  31. sonolus_py-0.3.2/tests/script/test_interval.py +0 -223
  32. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/.github/workflows/publish.yaml +0 -0
  33. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/.gitignore +0 -0
  34. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/.python-version +0 -0
  35. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/.run/Python tests in tests.run.xml +0 -0
  36. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/LICENSE +0 -0
  37. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/README.md +0 -0
  38. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/doc_stubs/__init__.py +0 -0
  39. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/doc_stubs/builtins.pyi +0 -0
  40. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/doc_stubs/math.pyi +0 -0
  41. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/doc_stubs/num.pyi +0 -0
  42. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/doc_stubs/random.pyi +0 -0
  43. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/CNAME +0 -0
  44. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/cli.md +0 -0
  45. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/constructs.md +0 -0
  46. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/index.md +0 -0
  47. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/project.md +0 -0
  48. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/concepts/resources.md +0 -0
  49. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/index.md +0 -0
  50. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/builtins.md +0 -0
  51. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/index.md +0 -0
  52. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/math.md +0 -0
  53. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/random.md +0 -0
  54. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.archetype.md +0 -0
  55. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.array.md +0 -0
  56. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.array_like.md +0 -0
  57. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.bucket.md +0 -0
  58. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.containers.md +0 -0
  59. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.debug.md +0 -0
  60. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.easing.md +0 -0
  61. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.effect.md +0 -0
  62. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.engine.md +0 -0
  63. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.globals.md +0 -0
  64. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.instruction.md +0 -0
  65. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.interval.md +0 -0
  66. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.iterator.md +0 -0
  67. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.level.md +0 -0
  68. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.metadata.md +0 -0
  69. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.num.md +0 -0
  70. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.options.md +0 -0
  71. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.particle.md +0 -0
  72. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.printing.md +0 -0
  73. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.project.md +0 -0
  74. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.quad.md +0 -0
  75. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.record.md +0 -0
  76. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.runtime.md +0 -0
  77. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.sprite.md +0 -0
  78. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.stream.md +0 -0
  79. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.text.md +0 -0
  80. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.timing.md +0 -0
  81. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.transform.md +0 -0
  82. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.ui.md +0 -0
  83. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.values.md +0 -0
  84. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/docs/reference/sonolus.script.vec.md +0 -0
  85. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/mkdocs.yml +0 -0
  86. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/scripts/generate.py +0 -0
  87. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/scripts/runtimes/Engine/Tutorial/Blocks.json +0 -0
  88. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/scripts/runtimes/Functions.json +0 -0
  89. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/scripts/runtimes/Level/Play/Blocks.json +0 -0
  90. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/scripts/runtimes/Level/Preview/Blocks.json +0 -0
  91. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/scripts/runtimes/Level/Watch/Blocks.json +0 -0
  92. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/__init__.py +0 -0
  93. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/__init__.py +0 -0
  94. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/blocks.py +0 -0
  95. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/excepthook.py +0 -0
  96. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/interpret.py +0 -0
  97. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/ir.py +0 -0
  98. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/mode.py +0 -0
  99. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/node.py +0 -0
  100. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/ops.py +0 -0
  101. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/__init__.py +0 -0
  102. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/allocate.py +0 -0
  103. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/constant_evaluation.py +0 -0
  104. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/copy_coalesce.py +0 -0
  105. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/dead_code.py +0 -0
  106. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/dominance.py +0 -0
  107. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/flow.py +0 -0
  108. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/inlining.py +0 -0
  109. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/liveness.py +0 -0
  110. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/optimize.py +0 -0
  111. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/passes.py +0 -0
  112. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/simplify.py +0 -0
  113. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/optimize/ssa.py +0 -0
  114. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/place.py +0 -0
  115. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/backend/utils.py +0 -0
  116. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/__init__.py +0 -0
  117. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/cli.py +0 -0
  118. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/collection.py +0 -0
  119. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/compile.py +0 -0
  120. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/engine.py +0 -0
  121. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/level.py +0 -0
  122. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/node.py +0 -0
  123. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/build/project.py +0 -0
  124. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/py.typed +0 -0
  125. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/__init__.py +0 -0
  126. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/bucket.py +0 -0
  127. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/debug.py +0 -0
  128. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/easing.py +0 -0
  129. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/effect.py +0 -0
  130. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/engine.py +0 -0
  131. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/globals.py +0 -0
  132. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/instruction.py +0 -0
  133. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/__init__.py +0 -0
  134. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/callbacks.py +0 -0
  135. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/constant.py +0 -0
  136. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/descriptor.py +0 -0
  137. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/dict_impl.py +0 -0
  138. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/error.py +0 -0
  139. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/generic.py +0 -0
  140. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/impl.py +0 -0
  141. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/introspection.py +0 -0
  142. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/math_impls.py +0 -0
  143. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/native.py +0 -0
  144. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/random.py +0 -0
  145. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/simulation_context.py +0 -0
  146. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/transient.py +0 -0
  147. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/internal/value.py +0 -0
  148. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/iterator.py +0 -0
  149. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/level.py +0 -0
  150. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/metadata.py +0 -0
  151. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/num.py +0 -0
  152. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/options.py +0 -0
  153. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/particle.py +0 -0
  154. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/pointer.py +0 -0
  155. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/printing.py +0 -0
  156. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/project.py +0 -0
  157. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/sprite.py +0 -0
  158. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/text.py +0 -0
  159. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/timing.py +0 -0
  160. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/ui.py +0 -0
  161. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/sonolus/script/values.py +0 -0
  162. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/__init__.py +0 -0
  163. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/__init__.py +0 -0
  164. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/conftest.py +0 -0
  165. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_assert.py +0 -0
  166. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_dict.py +0 -0
  167. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_flow.py +0 -0
  168. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_functions.py +0 -0
  169. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_helpers.py +0 -0
  170. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_match.py +0 -0
  171. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_num.py +0 -0
  172. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_quad.py +0 -0
  173. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_random.py +0 -0
  174. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_record.py +0 -0
  175. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_transform.py +0 -0
  176. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_values.py +0 -0
  177. {sonolus_py-0.3.2 → sonolus_py-0.3.3}/tests/script/test_vec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonolus.py
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Sonolus engine development in Python
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -2,7 +2,7 @@
2
2
  Sonolus.py comes with support for a number of built-in functions.
3
3
 
4
4
  - `abs(x)`
5
- - `bool(object)` (for a num argument)
5
+ - `bool(object)`
6
6
  - `callable(object)`
7
7
  - `enumerate(iterable, start=0)`
8
8
  - `filter(function, iterable)`
@@ -38,6 +38,10 @@ Sonolus.py also comes with support for some standard library modules.
38
38
  - `ceil(x)`
39
39
  - `trunc(x)`
40
40
  - `log(x[, base])`
41
+ - `pi`
42
+ - `e`
43
+ - `tau`
44
+ - `inf`
41
45
 
42
46
  ### random
43
47
  - `randrange(stop)`, `random.randrange(start, stop[, step])`
@@ -14,7 +14,7 @@ Sonolus.py will treat any of these types as `Num`, but it's recommended to use w
14
14
  The Sonolus app uses 32-bit floating-point numbers for all numeric values, so precision may be lower compared to Python
15
15
  when running on Sonolus.
16
16
 
17
- Infinity, NaN, and values outside the range of 32-bit floating-point numbers are not supported.
17
+ NaN and values outside the range of 32-bit floating-point numbers are not supported.
18
18
 
19
19
  You can import `Num` from `sonolus.script.num`:
20
20
 
@@ -42,8 +42,10 @@ Nums support most of the standard Python operations:
42
42
  Floating point precision may be lower when running on Sonolus compared to Python.
43
43
  Care should be taken when performing precision-sensitive operations.
44
44
 
45
- Nums are the only supported type for boolean operations and control flow conditions.
46
- As a condition, any nonzero value is considered true, and `0` is considered false.
45
+ As in regular Python, `0` is considered `False`, while any non-zero value is considered `True`.
46
+
47
+ Objects with an explicit `__bool__` method may also be used in `if`, `while`, `case ... if` expressions as well as with
48
+ the `not` operator. However, the operands of the `and` and `or` operators must be of type `Num`.
47
49
 
48
50
  - Logical operators: `and`, `or`, `not`
49
51
  - Ternary expressions: `... if <condition> else ...`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sonolus.py"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "Sonolus engine development in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,3 +1,5 @@
1
+ from math import isfinite, isinf, isnan
2
+
1
3
  from sonolus.backend.ir import IRConst, IRGet, IRInstr, IRPureInstr, IRSet
2
4
  from sonolus.backend.node import ConstantNode, EngineNode, FunctionNode
3
5
  from sonolus.backend.ops import Op
@@ -54,10 +56,20 @@ def cfg_to_engine_node(entry: BasicBlock):
54
56
 
55
57
  def ir_to_engine_node(stmt) -> EngineNode:
56
58
  match stmt:
57
- case int() | float():
58
- return ConstantNode(value=float(stmt))
59
- case IRConst(value=int(value) | float(value)):
60
- return ConstantNode(value=value)
59
+ case int(value) | float(value) | IRConst(value=int(value) | float(value)):
60
+ value = float(value)
61
+ if value.is_integer():
62
+ return ConstantNode(value=int(value))
63
+ elif isfinite(value):
64
+ return ConstantNode(value=value)
65
+ elif isinf(value):
66
+ # Read values from ROM
67
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=1 if value > 0 else 2)])
68
+ elif isnan(value):
69
+ # Read value from ROM
70
+ return FunctionNode(Op.Get, args=[ConstantNode(value=3000), ConstantNode(value=0)])
71
+ else:
72
+ raise ValueError(f"Invalid constant value: {value}")
61
73
  case IRPureInstr(op=op, args=args) | IRInstr(op=op, args=args):
62
74
  return FunctionNode(func=op, args=[ir_to_engine_node(arg) for arg in args])
63
75
  case IRGet(place=place):
@@ -359,7 +359,7 @@ class Visitor(ast.NodeVisitor):
359
359
  self.loop_head_ctxs.append(header_ctx)
360
360
  self.break_ctxs.append([])
361
361
  set_ctx(header_ctx)
362
- has_next = self.ensure_boolean_num(self.handle_call(node, iterator.has_next))
362
+ has_next = self.convert_to_boolean_num(node, self.handle_call(node, iterator.has_next))
363
363
  if has_next._is_py_() and not has_next._as_py_():
364
364
  # The loop will never run, continue after evaluating the condition
365
365
  self.loop_head_ctxs.pop()
@@ -400,7 +400,7 @@ class Visitor(ast.NodeVisitor):
400
400
  self.loop_head_ctxs.append(header_ctx)
401
401
  self.break_ctxs.append([])
402
402
  set_ctx(header_ctx)
403
- test = self.ensure_boolean_num(self.visit(node.test))
403
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
404
404
  if test._is_py_():
405
405
  if test._as_py_():
406
406
  # The loop will run until a break / return
@@ -454,7 +454,7 @@ class Visitor(ast.NodeVisitor):
454
454
  set_ctx(after_ctx)
455
455
 
456
456
  def visit_If(self, node):
457
- test = self.ensure_boolean_num(self.visit(node.test))
457
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
458
458
 
459
459
  if test._is_py_():
460
460
  if test._as_py_():
@@ -507,7 +507,9 @@ class Visitor(ast.NodeVisitor):
507
507
  set_ctx(false_ctx)
508
508
  continue
509
509
  set_ctx(true_ctx)
510
- guard = self.ensure_boolean_num(self.visit(case.guard)) if case.guard else validate_value(True)
510
+ guard = (
511
+ self.convert_to_boolean_num(case.guard, self.visit(case.guard)) if case.guard else validate_value(True)
512
+ )
511
513
  if guard._is_py_():
512
514
  if guard._as_py_():
513
515
  for stmt in case.body:
@@ -544,7 +546,7 @@ class Visitor(ast.NodeVisitor):
544
546
  match pattern:
545
547
  case ast.MatchValue(value=value):
546
548
  value = self.visit(value)
547
- test = self.ensure_boolean_num(validate_value(subject == value))
549
+ test = self.convert_to_boolean_num(pattern, validate_value(subject == value))
548
550
  if test._is_py_():
549
551
  if test._as_py_():
550
552
  return ctx(), ctx().into_dead()
@@ -574,7 +576,7 @@ class Visitor(ast.NodeVisitor):
574
576
  target_len = len(patterns)
575
577
  if not (isinstance(subject, Sequence | TupleImpl)):
576
578
  return ctx().into_dead(), ctx()
577
- length_test = self.ensure_boolean_num(validate_value(_len(subject) == target_len))
579
+ length_test = self.convert_to_boolean_num(pattern, validate_value(_len(subject) == target_len))
578
580
  ctx_init = ctx()
579
581
  if not length_test._is_py_():
580
582
  ctx_init.test = length_test.ir()
@@ -739,7 +741,7 @@ class Visitor(ast.NodeVisitor):
739
741
  def visit_UnaryOp(self, node):
740
742
  operand = self.visit(node.operand)
741
743
  if isinstance(node.op, ast.Not):
742
- return self.ensure_boolean_num(operand).not_()
744
+ return self.convert_to_boolean_num(node, operand).not_()
743
745
  op = unary_ops[type(node.op)]
744
746
  if operand._is_py_():
745
747
  operand_py = operand._as_py_()
@@ -768,7 +770,7 @@ class Visitor(ast.NodeVisitor):
768
770
  return validate_value(fn)
769
771
 
770
772
  def visit_IfExp(self, node):
771
- test = self.ensure_boolean_num(self.visit(node.test))
773
+ test = self.convert_to_boolean_num(node.test, self.visit(node.test))
772
774
 
773
775
  if test._is_py_():
774
776
  if test._as_py_():
@@ -1076,7 +1078,7 @@ class Visitor(ast.NodeVisitor):
1076
1078
 
1077
1079
  def handle_call[**P, R](
1078
1080
  self, node: ast.stmt | ast.expr, fn: Callable[P, R], /, *args: P.args, **kwargs: P.kwargs
1079
- ) -> R:
1081
+ ) -> R | Value:
1080
1082
  """Handles a call to the given callable."""
1081
1083
  self.active_ctx = ctx()
1082
1084
  if (
@@ -1127,6 +1129,20 @@ class Visitor(ast.NodeVisitor):
1127
1129
  raise TypeError(f"Invalid type where a bool (Num) was expected: {type(value).__name__}")
1128
1130
  return value
1129
1131
 
1132
+ def convert_to_boolean_num(self, node, value: Value) -> Num:
1133
+ if _is_num(value):
1134
+ return value
1135
+ if hasattr(type(value), "__bool__"):
1136
+ return self.ensure_boolean_num(self.handle_call(node, type(value).__bool__, validate_value(value)))
1137
+ if hasattr(type(value), "__len__"):
1138
+ length = self.handle_call(node, type(value).__len__, validate_value(value))
1139
+ if not _is_num(length):
1140
+ raise TypeError(f"Invalid type for __len__: {type(length).__name__}")
1141
+ if length._is_py_():
1142
+ return Num._accept_(length._as_py_() > 0)
1143
+ return length > Num._accept_(0)
1144
+ raise TypeError(f"Converting {type(value).__name__} to bool is not supported")
1145
+
1130
1146
  def arguments_to_signature(self, arguments: ast.arguments) -> inspect.Signature:
1131
1147
  parameters: list[inspect.Parameter] = []
1132
1148
  pos_only_count = len(arguments.posonlyargs)
@@ -549,11 +549,19 @@ class _BaseArchetype:
549
549
  metadata = _annotation_defaults.get(metadata, metadata)
550
550
  if isinstance(metadata, _ArchetypeFieldInfo):
551
551
  if field_info is not None:
552
- raise TypeError(
553
- f"Unexpected multiple field annotations for '{name}', "
554
- f"expected exactly one of imported, exported, entity_memory, or shared_memory"
555
- )
556
- field_info = metadata
552
+ if field_info.storage == metadata.storage and field_info.name is None:
553
+ field_info = metadata
554
+ elif field_info.storage == metadata.storage and (
555
+ metadata.name is None or field_info.name == metadata.name
556
+ ):
557
+ pass
558
+ else:
559
+ raise TypeError(
560
+ f"Unexpected multiple field annotations for '{name}', "
561
+ f"expected exactly one of imported, exported, entity_memory, or shared_memory"
562
+ )
563
+ else:
564
+ field_info = metadata
557
565
  if field_info is None:
558
566
  raise TypeError(
559
567
  f"Missing field annotation for '{name}', "
@@ -880,7 +888,7 @@ class WatchArchetype(_BaseArchetype):
880
888
  case _ArchetypeSelfData():
881
889
  return _deref(ctx().blocks.EntityInfo, 0, WatchEntityInfo)
882
890
  case _ArchetypeReferenceData(index=index):
883
- return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), PlayEntityInfo)
891
+ return _deref(ctx().blocks.EntityInfoArray, index * WatchEntityInfo._size_(), WatchEntityInfo)
884
892
  case _:
885
893
  raise RuntimeError("Info is only accessible from the entity itself")
886
894
 
@@ -1106,10 +1114,20 @@ class EntityRef[A: _BaseArchetype](Record):
1106
1114
  """Return a new reference with the given archetype type."""
1107
1115
  return EntityRef[archetype](index=self.index)
1108
1116
 
1117
+ @meta_fn
1109
1118
  def get(self) -> A:
1110
1119
  """Get the entity."""
1120
+ if ref := getattr(self, "_ref_", None):
1121
+ return ref
1111
1122
  return self.archetype().at(self.index)
1112
1123
 
1124
+ @meta_fn
1125
+ def get_as(self, archetype: type[_BaseArchetype]) -> _BaseArchetype:
1126
+ """Get the entity as the given archetype type."""
1127
+ if getattr(archetype, "_ref_", None):
1128
+ raise TypeError("Using get_as in level data is not supported.")
1129
+ return self.with_archetype(archetype).get()
1130
+
1113
1131
  def archetype_matches(self) -> bool:
1114
1132
  """Check if entity at the index is precisely of the archetype."""
1115
1133
  return self.index >= 0 and self.archetype().is_at(self.index)
@@ -1117,7 +1135,7 @@ class EntityRef[A: _BaseArchetype](Record):
1117
1135
  def _to_list_(self, level_refs: dict[Any, str] | None = None) -> list[DataValue | str]:
1118
1136
  ref = getattr(self, "_ref_", None)
1119
1137
  if ref is None:
1120
- return [self.index]
1138
+ return Num._accept_(self.index)._to_list_()
1121
1139
  else:
1122
1140
  if ref not in level_refs:
1123
1141
  raise KeyError("Reference to entity not in level data")
@@ -5,7 +5,7 @@ from typing import Any, Self, final
5
5
 
6
6
  from sonolus.backend.ir import IRConst, IRSet
7
7
  from sonolus.backend.place import BlockPlace
8
- from sonolus.script.array_like import ArrayLike
8
+ from sonolus.script.array_like import ArrayLike, get_positive_index
9
9
  from sonolus.script.debug import assert_unreachable
10
10
  from sonolus.script.internal.context import ctx
11
11
  from sonolus.script.internal.error import InternalError
@@ -18,6 +18,7 @@ from sonolus.script.num import Num
18
18
  class ArrayMeta(type):
19
19
  @meta_fn
20
20
  def __pos__[T](cls: type[T]) -> T:
21
+ """Create a zero-initialized array instance."""
21
22
  return cls._zero_()
22
23
 
23
24
 
@@ -186,7 +187,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
186
187
 
187
188
  @meta_fn
188
189
  def __getitem__(self, index: Num) -> T:
189
- index: Num = Num._accept_(index)
190
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
190
191
  if index._is_py_() and 0 <= index._as_py_() < self.size():
191
192
  const_index = index._as_py_()
192
193
  if isinstance(const_index, float) and not const_index.is_integer():
@@ -230,7 +231,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
230
231
 
231
232
  @meta_fn
232
233
  def __setitem__(self, index: Num, value: T):
233
- index: Num = Num._accept_(index)
234
+ index: Num = Num._accept_(get_positive_index(index, self.size()))
234
235
  value = self.element_type()._accept_(value)
235
236
  if ctx():
236
237
  if isinstance(self._value, list):
@@ -303,6 +304,7 @@ class Array[T, Size](GenericValue, ArrayLike[T], metaclass=ArrayMeta):
303
304
  else:
304
305
  return f"{type(self).__name__}({', '.join(repr(self[i]) for i in range(self.size()))})"
305
306
 
307
+ @meta_fn
306
308
  def __pos__(self) -> Self:
307
309
  """Return a copy of the array."""
308
310
  return self._copy_()
@@ -5,6 +5,8 @@ from abc import abstractmethod
5
5
  from collections.abc import Callable
6
6
  from typing import Any
7
7
 
8
+ from sonolus.script.internal.context import ctx
9
+ from sonolus.script.internal.impl import meta_fn
8
10
  from sonolus.script.iterator import SonolusIterator
9
11
  from sonolus.script.num import Num
10
12
  from sonolus.script.record import Record
@@ -302,3 +304,26 @@ class _ArrayEnumerator[V: ArrayLike](Record, SonolusIterator):
302
304
 
303
305
  def advance(self):
304
306
  self.i += 1
307
+
308
+
309
+ @meta_fn
310
+ def get_positive_index(index: Num, length: Num) -> Num:
311
+ """Get the positive index for the given index in the array of the given length.
312
+
313
+ This is used to convert negative indixes relative to the end of the array to positive indices.
314
+
315
+ Args:
316
+ index: The index to convert.
317
+ length: The length of the array.
318
+
319
+ Returns:
320
+ The positive index.
321
+ """
322
+ if not ctx():
323
+ return index if index >= 0 else index + length
324
+ index = Num._accept_(index)
325
+ length = Num._accept_(length)
326
+ if index._is_py_() and length._is_py_():
327
+ return Num._accept_(index._as_py_() + length._as_py_() if index._as_py_() < 0 else index._as_py_())
328
+ else:
329
+ return index + (index < 0) * length
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from sonolus.backend.visitor import compile_and_call
4
4
  from sonolus.script.array import Array
5
- from sonolus.script.array_like import ArrayLike
5
+ from sonolus.script.array_like import ArrayLike, get_positive_index
6
6
  from sonolus.script.debug import error
7
7
  from sonolus.script.internal.context import ctx
8
8
  from sonolus.script.internal.impl import meta_fn
9
+ from sonolus.script.interval import clamp
9
10
  from sonolus.script.iterator import SonolusIterator
10
11
  from sonolus.script.num import Num
11
12
  from sonolus.script.pointer import _deref
@@ -140,11 +141,11 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
140
141
  assert p == Pair(5, 6) # The value of p has changed
141
142
  ```
142
143
  """
143
- return self._array[item]
144
+ return self._array[get_positive_index(item, len(self))]
144
145
 
145
146
  def __setitem__(self, key: int, value: T):
146
147
  """Update the element at the given index."""
147
- self._array[key] = value
148
+ self._array[get_positive_index(key, len(self))] = value
148
149
 
149
150
  def __delitem__(self, key: int):
150
151
  """Remove the element at the given index."""
@@ -190,6 +191,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
190
191
  """
191
192
  if index is None:
192
193
  index = self._size - 1
194
+ index = get_positive_index(index, len(self))
193
195
  assert 0 <= index < self._size
194
196
  value = copy(self._array[index])
195
197
  self._size -= 1
@@ -207,7 +209,7 @@ class VarArray[T, Capacity](Record, ArrayLike[T]):
207
209
  index: The index at which to insert the value. Must be in the range [0, size].
208
210
  value: The value to insert.
209
211
  """
210
- assert 0 <= index <= self._size
212
+ index = clamp(get_positive_index(index, len(self)), 0, self._size)
211
213
  assert self._size < len(self._array)
212
214
  self._size += 1
213
215
  for i in range(self._size - 1, index, -1):
@@ -329,6 +331,7 @@ class ArrayPointer[T](Record, ArrayLike[T]):
329
331
 
330
332
  @meta_fn
331
333
  def _get_item(self, item: int) -> T:
334
+ item = get_positive_index(item, self.size)
332
335
  if not ctx():
333
336
  raise TypeError("ArrayPointer values cannot be accessed outside of a context")
334
337
  return _deref(
@@ -536,6 +539,23 @@ class ArrayMap[K, V, Capacity](Record):
536
539
  self._array[self._size] = _ArrayMapEntry(key, value)
537
540
  self._size += 1
538
541
 
542
+ def __delitem__(self, key: K):
543
+ """Remove the key-value pair associated with the given key.
544
+
545
+ Must be called with a key that is present in the map.
546
+
547
+ Args:
548
+ key: The key to remove
549
+ """
550
+ for i in range(self._size):
551
+ entry = self._array[i]
552
+ if entry.key == key:
553
+ self._size -= 1
554
+ if i < self._size:
555
+ self._array[i] = self._array[self._size]
556
+ return
557
+ error()
558
+
539
559
  def __contains__(self, key: K) -> bool:
540
560
  """Return whether the given key is present in the map.
541
561
 
@@ -1,6 +1,7 @@
1
1
  from collections.abc import Iterable
2
2
  from typing import overload
3
3
 
4
+ from sonolus.script.array import Array
4
5
  from sonolus.script.array_like import ArrayLike
5
6
  from sonolus.script.internal.context import ctx
6
7
  from sonolus.script.internal.dict_impl import DictImpl
@@ -129,6 +130,8 @@ def _max(*args, key: callable = _identity):
129
130
  (iterable,) = args
130
131
  if isinstance(iterable, ArrayLike):
131
132
  return compile_and_call(iterable._max_, key=key)
133
+ elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
134
+ return compile_and_call(Array(*iterable.value)._max_, key=key)
132
135
  else:
133
136
  raise TypeError(f"Unsupported type: {type(iterable)} for max")
134
137
  else:
@@ -169,6 +172,8 @@ def _min(*args, key: callable = _identity):
169
172
  (iterable,) = args
170
173
  if isinstance(iterable, ArrayLike):
171
174
  return compile_and_call(iterable._min_, key=key)
175
+ elif isinstance(iterable, TupleImpl) and all(_is_num(v) for v in iterable.value):
176
+ return compile_and_call(Array(*iterable.value)._min_, key=key)
172
177
  else:
173
178
  raise TypeError(f"Unsupported type: {type(iterable)} for min")
174
179
  else:
@@ -221,12 +226,12 @@ def _float(value=0.0):
221
226
  return value
222
227
 
223
228
 
224
- @meta_fn
225
229
  def _bool(value=False):
226
- value = validate_value(value)
227
- if not _is_num(value):
228
- raise TypeError("Only numeric arguments to bool() are supported")
229
- return value != 0
230
+ # Relies on the compiler to perform the conversion in a boolean context
231
+ if value: # noqa: SIM103
232
+ return True
233
+ else:
234
+ return False
230
235
 
231
236
 
232
237
  _int._type_mapping_ = Num
@@ -291,7 +291,11 @@ class ReadOnlyMemory:
291
291
  _lock: Lock
292
292
 
293
293
  def __init__(self):
294
- self.values = []
294
+ self.values = [
295
+ float("nan"),
296
+ float("inf"),
297
+ float("-inf"),
298
+ ]
295
299
  self.indexes = {}
296
300
  self._lock = Lock()
297
301
 
@@ -1,4 +1,6 @@
1
- from sonolus.script.array_like import ArrayLike
1
+ from sonolus.script.array_like import ArrayLike, get_positive_index
2
+ from sonolus.script.internal.context import ctx
3
+ from sonolus.script.internal.impl import meta_fn, validate_value
2
4
  from sonolus.script.iterator import SonolusIterator
3
5
  from sonolus.script.num import Num
4
6
  from sonolus.script.record import Record
@@ -36,7 +38,7 @@ class Range(Record, ArrayLike[Num]):
36
38
  return (diff - self.step - 1) // -self.step
37
39
 
38
40
  def __getitem__(self, index: Num) -> Num:
39
- return self.start + index * self.step
41
+ return self.start + get_positive_index(index, len(self)) * self.step
40
42
 
41
43
  def __setitem__(self, index: Num, value: Num):
42
44
  raise TypeError("Range does not support item assignment")
@@ -79,3 +81,24 @@ class RangeIterator(Record, SonolusIterator):
79
81
 
80
82
  def advance(self):
81
83
  self.value += self.step
84
+
85
+
86
+ @meta_fn
87
+ def range_or_tuple(start: Num, stop: Num | None = None, step: Num = 1) -> Range | tuple[Num, ...]:
88
+ if stop is None:
89
+ start, stop = 0, start
90
+ if not ctx():
91
+ return range(start, stop, step) # type: ignore
92
+ start = Num._accept_(start)
93
+ stop = Num._accept_(stop) if stop is not None else None
94
+ step = Num._accept_(step)
95
+ if start._is_py_() and stop._is_py_() and step._is_py_():
96
+ start_int = start._as_py_()
97
+ stop_int = stop._as_py_() if stop is not None else None
98
+ if stop_int is None:
99
+ start_int, stop_int = 0, start_int
100
+ step_int = step._as_py_()
101
+ if start_int % 1 != 0 or stop_int % 1 != 0 or step_int % 1 != 0:
102
+ raise TypeError("Range arguments must be integers")
103
+ return validate_value(tuple(range(int(start_int), int(stop_int), int(step_int)))) # type: ignore
104
+ return Range(start, stop, step)
@@ -21,8 +21,10 @@ class TupleImpl(TransientValue):
21
21
  raise TypeError(f"Cannot index tuple with {item}")
22
22
  if int(item) != item:
23
23
  raise TypeError(f"Cannot index tuple with non-integer {item}")
24
- if not (0 <= item < len(self.value)):
24
+ if not (-len(self.value) <= item < len(self.value)):
25
25
  raise IndexError(f"Tuple index out of range: {item}")
26
+ if item < 0:
27
+ item += len(self.value)
26
28
  return self.value[int(item)]
27
29
 
28
30
  @meta_fn
@@ -1,8 +1,10 @@
1
1
  from typing import Self
2
2
 
3
3
  from sonolus.backend.ops import Op
4
+ from sonolus.script.array_like import ArrayLike
4
5
  from sonolus.script.debug import static_error
5
6
  from sonolus.script.internal.native import native_function
7
+ from sonolus.script.internal.range import range_or_tuple
6
8
  from sonolus.script.num import Num
7
9
  from sonolus.script.record import Record
8
10
 
@@ -296,7 +298,7 @@ def unlerp_clamped(a: float, b: float, x: float, /) -> float:
296
298
 
297
299
  @native_function(Op.Remap)
298
300
  def remap(a: float, b: float, c: float, d: float, x: float, /) -> float:
299
- """Remap a value from one interval to another.
301
+ """Linearly remap a value from one interval to another.
300
302
 
301
303
  Args:
302
304
  a: The start of the input interval.
@@ -313,7 +315,7 @@ def remap(a: float, b: float, c: float, d: float, x: float, /) -> float:
313
315
 
314
316
  @native_function(Op.RemapClamped)
315
317
  def remap_clamped(a: float, b: float, c: float, d: float, x: float, /) -> float:
316
- """Remap a value from one interval to another, clamped to the output interval.
318
+ """Linearly remap a value from one interval to another, clamped to the output interval.
317
319
 
318
320
  Args:
319
321
  a: The start of the input interval.
@@ -341,3 +343,59 @@ def clamp(x: float, a: float, b: float, /) -> float:
341
343
  The clamped value.
342
344
  """
343
345
  return max(a, min(b, x))
346
+
347
+
348
+ def interp(
349
+ xp: ArrayLike[float] | tuple[float, ...],
350
+ fp: ArrayLike[float] | tuple[float, ...],
351
+ x: float,
352
+ ) -> float:
353
+ """Linearly interpolate a value within a sequence of points.
354
+
355
+ The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.
356
+ For values of x outside the range of xp, the slope of the first or last segment is used to extrapolate.
357
+
358
+ Args:
359
+ xp: The x-coordinates of the points in increasing order.
360
+ fp: The y-coordinates of the points.
361
+ x: The x-coordinate to interpolate.
362
+
363
+ Returns:
364
+ The interpolated value.
365
+ """
366
+ assert len(xp) == len(fp)
367
+ assert len(xp) >= 2
368
+ for i in range_or_tuple(1, len(xp) - 1):
369
+ # At i == 1, x may be less than x[0], but since we're extrapolating, we use the first segment regardless.
370
+ if x <= xp[i]:
371
+ return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
372
+ # x > xp[-2] so we can just use the last segment regardless of whether x is in it or to the right of it.
373
+ return remap(xp[-2], xp[-1], fp[-2], fp[-1], x)
374
+
375
+
376
+ def interp_clamped(
377
+ xp: ArrayLike[float] | tuple[float, ...],
378
+ fp: ArrayLike[float] | tuple[float, ...],
379
+ x: float,
380
+ ):
381
+ """Linearly interpolate a value within a sequence of points.
382
+
383
+ The sequence must have at least 2 elements and be sorted in increasing order of x-coordinates.
384
+ For x-coordinates outside the range of the sequence, the respective endpoint of fp is returned.
385
+
386
+ Args:
387
+ xp: The x-coordinates of the points in increasing order.
388
+ fp: The y-coordinates of the points.
389
+ x: The x-coordinate to interpolate.
390
+
391
+ Returns:
392
+ The interpolated value.
393
+ """
394
+ assert len(xp) == len(fp)
395
+ assert len(xp) >= 2
396
+ if x <= xp[0]:
397
+ return fp[0]
398
+ for i in range_or_tuple(1, len(xp)):
399
+ if x <= xp[i]:
400
+ return remap(xp[i - 1], xp[i], fp[i - 1], fp[i], x)
401
+ return fp[-1]