fpy2 0.2.1__tar.gz → 0.2.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.
Files changed (198) hide show
  1. {fpy2-0.2.1 → fpy2-0.2.3}/PKG-INFO +1 -1
  2. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/__init__.py +13 -1
  3. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/__init__.py +15 -1
  4. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/alias.py +295 -9
  5. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/array_size.py +64 -21
  6. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/call_graph.py +0 -6
  7. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/context_use.py +18 -10
  8. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/define_use.py +30 -4
  9. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/format_infer/__init__.py +22 -0
  10. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/format_infer/analysis.py +591 -43
  11. fpy2-0.2.3/fpy2/analysis/format_infer/double_round.py +288 -0
  12. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/format_infer/format.py +180 -66
  13. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/live_vars.py +2 -0
  14. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/partial_eval.py +31 -38
  15. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/purity.py +0 -4
  16. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/reachability.py +20 -9
  17. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/reaching_defs.py +22 -0
  18. fpy2-0.2.3/fpy2/analysis/storage_infer.py +332 -0
  19. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/syntax_check.py +55 -6
  20. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/type_infer.py +18 -4
  21. fpy2-0.2.3/fpy2/analysis/value_class.py +781 -0
  22. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/ast/formatter.py +47 -25
  23. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/ast/visitor.py +2 -0
  24. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/compiler.py +153 -35
  25. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/emitter.py +1217 -397
  26. fpy2-0.2.3/fpy2/backend/cpp/storage.py +269 -0
  27. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/target.py +13 -0
  28. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/unbox.py +62 -163
  29. fpy2-0.2.3/fpy2/backend/cpp/unfold_round.py +389 -0
  30. fpy2-0.2.3/fpy2/backend/cpp/utils.py +43 -0
  31. fpy2-0.2.1/fpy2/backend/cpp/storage_infer.py → fpy2-0.2.3/fpy2/backend/cpp/variables.py +119 -199
  32. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/fpc.py +81 -46
  33. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/env.py +19 -1
  34. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/function.py +62 -2
  35. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/interpret/__init__.py +1 -0
  36. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/interpret/byte.py +143 -86
  37. fpy2-0.2.3/fpy2/interpret/value.py +174 -0
  38. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/core.py +11 -34
  39. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/__init__.py +1 -1
  40. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/context.py +21 -1
  41. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/efloat.py +18 -14
  42. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/exponential.py +3 -0
  43. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/mp_fixed.py +9 -11
  44. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/mp_float.py +163 -19
  45. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/mpb_fixed.py +16 -12
  46. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/mpb_float.py +168 -19
  47. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/mps_float.py +171 -19
  48. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/real.py +3 -0
  49. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/engine/real.py +92 -14
  50. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/number/__init__.py +1 -1
  51. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/number/floats.py +17 -0
  52. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/number/reals.py +9 -3
  53. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/ops.py +4 -4
  54. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/primitive.py +6 -22
  55. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/rewrite/__init__.py +2 -1
  56. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/rewrite/applier.py +24 -29
  57. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/rewrite/matcher.py +60 -102
  58. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/rewrite/pattern.py +10 -0
  59. fpy2-0.2.3/fpy2/rewrite/rewrite.py +313 -0
  60. fpy2-0.2.3/fpy2/rewrite/search.py +120 -0
  61. fpy2-0.2.3/fpy2/strategies/__init__.py +86 -0
  62. fpy2-0.2.3/fpy2/strategies/anf.py +69 -0
  63. fpy2-0.2.3/fpy2/strategies/comp_lower.py +108 -0
  64. fpy2-0.2.3/fpy2/strategies/context_lift.py +60 -0
  65. fpy2-0.2.3/fpy2/strategies/fixed_rescale.py +91 -0
  66. fpy2-0.2.3/fpy2/strategies/float_lower.py +103 -0
  67. fpy2-0.2.3/fpy2/strategies/free_var.py +54 -0
  68. fpy2-0.2.3/fpy2/strategies/func_inline.py +97 -0
  69. fpy2-0.2.3/fpy2/strategies/hoistable.py +85 -0
  70. fpy2-0.2.3/fpy2/strategies/iter_elim.py +92 -0
  71. fpy2-0.2.3/fpy2/strategies/iter_unfold.py +161 -0
  72. fpy2-0.2.3/fpy2/strategies/loop_split.py +108 -0
  73. fpy2-0.2.3/fpy2/strategies/loop_unroll.py +146 -0
  74. fpy2-0.2.3/fpy2/strategies/mono.py +88 -0
  75. fpy2-0.2.3/fpy2/strategies/neg_zero_unfold.py +101 -0
  76. fpy2-0.2.3/fpy2/strategies/overflow_unfold.py +114 -0
  77. fpy2-0.2.3/fpy2/strategies/reduce_fusion.py +49 -0
  78. fpy2-0.2.3/fpy2/strategies/round_elim.py +60 -0
  79. fpy2-0.2.3/fpy2/strategies/round_insert.py +108 -0
  80. fpy2-0.2.3/fpy2/strategies/round_split.py +123 -0
  81. fpy2-0.2.3/fpy2/strategies/simple.py +63 -0
  82. fpy2-0.2.3/fpy2/strategies/sites.py +224 -0
  83. fpy2-0.2.3/fpy2/strategies/special_unfold.py +114 -0
  84. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/__init__.py +32 -0
  85. fpy2-0.2.3/fpy2/transform/anf.py +481 -0
  86. fpy2-0.2.3/fpy2/transform/comp_to_loop.py +558 -0
  87. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/const_fold.py +20 -3
  88. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/copy_propagate.py +2 -2
  89. fpy2-0.2.3/fpy2/transform/cursor.py +440 -0
  90. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/dead_code.py +112 -25
  91. fpy2-0.2.3/fpy2/transform/error.py +24 -0
  92. fpy2-0.2.3/fpy2/transform/float_to_fixed.py +617 -0
  93. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/for_unroll.py +141 -96
  94. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/free_var_elim.py +13 -2
  95. fpy2-0.2.3/fpy2/transform/func_inline.py +411 -0
  96. fpy2-0.2.3/fpy2/transform/hoistable.py +584 -0
  97. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/iter_elim.py +1 -6
  98. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/lift_context.py +18 -1
  99. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/monomorphize.py +19 -3
  100. fpy2-0.2.3/fpy2/transform/path.py +339 -0
  101. fpy2-0.2.3/fpy2/transform/rescale_fixed.py +553 -0
  102. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/round_elim.py +28 -33
  103. fpy2-0.2.3/fpy2/transform/round_insert.py +189 -0
  104. fpy2-0.2.3/fpy2/transform/simplify.py +88 -0
  105. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/specialize.py +6 -4
  106. fpy2-0.2.3/fpy2/transform/split_loop.py +504 -0
  107. fpy2-0.2.3/fpy2/transform/split_round.py +395 -0
  108. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/subst_var.py +20 -1
  109. fpy2-0.2.3/fpy2/transform/unfold_iter.py +242 -0
  110. fpy2-0.2.3/fpy2/transform/unfold_neg_zero.py +357 -0
  111. fpy2-0.2.3/fpy2/transform/unfold_overflow.py +664 -0
  112. fpy2-0.2.3/fpy2/transform/unfold_special.py +512 -0
  113. fpy2-0.2.3/fpy2/transform/utils.py +809 -0
  114. fpy2-0.2.3/fpy2/transform/while_unroll.py +117 -0
  115. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/decorator.py +8 -5
  116. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2.egg-info/PKG-INFO +1 -1
  117. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2.egg-info/SOURCES.txt +42 -1
  118. {fpy2-0.2.1 → fpy2-0.2.3}/pyproject.toml +1 -1
  119. fpy2-0.2.1/fpy2/backend/cpp/storage.py +0 -240
  120. fpy2-0.2.1/fpy2/backend/cpp/utils.py +0 -66
  121. fpy2-0.2.1/fpy2/rewrite/rewrite.py +0 -235
  122. fpy2-0.2.1/fpy2/strategies/__init__.py +0 -14
  123. fpy2-0.2.1/fpy2/strategies/loop_split.py +0 -35
  124. fpy2-0.2.1/fpy2/strategies/loop_unroll.py +0 -73
  125. fpy2-0.2.1/fpy2/strategies/simple.py +0 -53
  126. fpy2-0.2.1/fpy2/transform/func_inline.py +0 -243
  127. fpy2-0.2.1/fpy2/transform/split_loop.py +0 -216
  128. fpy2-0.2.1/fpy2/transform/while_unroll.py +0 -110
  129. {fpy2-0.2.1 → fpy2-0.2.3}/LICENSE +0 -0
  130. {fpy2-0.2.1 → fpy2-0.2.3}/README.md +0 -0
  131. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/defs.py +0 -0
  132. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/analysis/escape.py +0 -0
  133. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/ast/__init__.py +0 -0
  134. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/ast/fpyast.py +0 -0
  135. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/__init__.py +0 -0
  136. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/backend.py +0 -0
  137. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/__init__.py +0 -0
  138. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/ops.py +0 -0
  139. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/backend/cpp/types.py +0 -0
  140. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/decorator.py +0 -0
  141. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/fpc_context.py +0 -0
  142. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/frontend/__init__.py +0 -0
  143. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/frontend/fpc.py +0 -0
  144. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/frontend/parser.py +0 -0
  145. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/interpret/interpreter.py +0 -0
  146. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/__init__.py +0 -0
  147. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/base.py +0 -0
  148. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/eft.py +0 -0
  149. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/matrix.py +0 -0
  150. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/metrics.py +0 -0
  151. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/libraries/vector.py +0 -0
  152. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/module.py +0 -0
  153. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/__init__.py +0 -0
  154. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/fixed.py +0 -0
  155. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/format.py +0 -0
  156. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/ieee754.py +0 -0
  157. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/context/sm_fixed.py +0 -0
  158. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/engine/__init__.py +0 -0
  159. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/engine/engine.py +0 -0
  160. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/engine/gmp.py +0 -0
  161. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/format.py +0 -0
  162. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/globals.py +0 -0
  163. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/gmputils.py +0 -0
  164. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/native.py +0 -0
  165. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/number/flags.py +0 -0
  166. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/number/round.py +0 -0
  167. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/rewrite/subst.py +0 -0
  168. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/runner.py +0 -0
  169. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/enumerate_elim.py +0 -0
  170. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/for_bundling.py +0 -0
  171. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/for_unpack.py +0 -0
  172. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/if_bundling.py +0 -0
  173. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/reduce_fusion.py +0 -0
  174. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/rename_target.py +0 -0
  175. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/simplify_if.py +0 -0
  176. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/while_bundling.py +0 -0
  177. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/transform/zip_elim.py +0 -0
  178. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/types.py +0 -0
  179. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/__init__.py +0 -0
  180. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/bits.py +0 -0
  181. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/compare.py +0 -0
  182. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/default.py +0 -0
  183. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/float_params.py +0 -0
  184. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/fractions.py +0 -0
  185. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/gensym.py +0 -0
  186. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/identifier.py +0 -0
  187. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/inspect.py +0 -0
  188. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/iterator.py +0 -0
  189. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/loader.py +0 -0
  190. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/location.py +0 -0
  191. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/ordering.py +0 -0
  192. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/string.py +0 -0
  193. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/uninit.py +0 -0
  194. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2/utils/unionfind.py +0 -0
  195. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2.egg-info/dependency_links.txt +0 -0
  196. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2.egg-info/requires.txt +0 -0
  197. {fpy2-0.2.1 → fpy2-0.2.3}/fpy2.egg-info/top_level.txt +0 -0
  198. {fpy2-0.2.1 → fpy2-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fpy2
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: An embedded DSL for numerical computing
5
5
  Author-email: Brett Saiki <bsaiki@cs.washington.edu>
6
6
  License-Expression: MIT
@@ -19,7 +19,17 @@ These number systems guarantee correct rounding via MPFR.
19
19
  # base library
20
20
  # standard library
21
21
  # submodules
22
- from . import analysis, ast, libraries, number, strategies, transform, types, utils
22
+ from . import (
23
+ analysis,
24
+ ast,
25
+ libraries,
26
+ number,
27
+ rewrite,
28
+ strategies,
29
+ transform,
30
+ types,
31
+ utils,
32
+ )
23
33
 
24
34
  # compiler
25
35
  from .backend import (
@@ -32,6 +42,7 @@ from .backend import (
32
42
  from .fpc_context import FPCoreContext, NoSuchContextError
33
43
  from .interpret import (
34
44
  BytecodeInterpreter,
45
+ Foreign,
35
46
  Interpreter,
36
47
  get_default_interpreter,
37
48
  set_default_interpreter,
@@ -40,6 +51,7 @@ from .libraries.base import *
40
51
 
41
52
  # module
42
53
  from .module import Module, ModuleCallGraph, ModuleEntry
54
+ from .rewrite import Rewrite, find, find_all
43
55
 
44
56
  # runner
45
57
  from .runner import Runner, RunnerWorkerTask
@@ -1,6 +1,6 @@
1
1
  """Program analyses for FPy programs"""
2
2
 
3
- from .alias import Alias, AliasAnalysis, AllocSite
3
+ from .alias import Alias, AliasAnalysis, AllocSite, Region, region_sizes
4
4
  from .array_size import (
5
5
  ArraySize,
6
6
  ArraySizeAnalysis,
@@ -35,5 +35,19 @@ from .reaching_defs import (
35
35
  ReachingDefs,
36
36
  ReachingDefsAnalysis,
37
37
  )
38
+ from .storage_infer import (
39
+ StorageAnalysis,
40
+ StorageDomain,
41
+ StorageInfer,
42
+ StorageSelectionError,
43
+ of_bound,
44
+ )
38
45
  from .syntax_check import FPySyntaxError, SyntaxCheck
39
46
  from .type_infer import TypeAnalysis, TypeInfer, TypeInferError
47
+ from .value_class import (
48
+ ValueClass,
49
+ ValueClassAnalysis,
50
+ ValueClassInfer,
51
+ class_of,
52
+ representable_classes,
53
+ )
@@ -80,12 +80,14 @@ Limitations
80
80
  One *summary* store for the whole function, not one per program point, so it is
81
81
  flow-insensitive: ``ys = xs`` marks ``xs`` shared even if ``ys`` is dead
82
82
  immediately afterwards, and a container part counts as a referrer even if the
83
- container itself is never read. Intraprocedural: a list handed to a call is
84
- shared outward, without asking whether the callee retains it. Index-insensitive:
85
- ``xs[0]`` and ``xs[1]`` are one part. All three cost precision, not soundness.
83
+ container itself is never read — except :attr:`AliasAnalysis.consumed_defs`,
84
+ which buys back one flow-sensitive fact under narrow guards. Intraprocedural: a
85
+ list handed to a call is shared outward, without asking whether the callee
86
+ retains it. Index-insensitive: ``xs[0]`` and ``xs[1]`` are one part. All three
87
+ cost precision, not soundness.
86
88
  """
87
89
 
88
- from dataclasses import dataclass
90
+ from dataclasses import dataclass, field
89
91
  from typing import TYPE_CHECKING, Literal, overload
90
92
 
91
93
  from ..ast import (
@@ -112,16 +114,26 @@ from ..ast import (
112
114
  Range3,
113
115
  ReturnStmt,
114
116
  Snd,
117
+ Stmt,
118
+ StmtBlock,
115
119
  TupleBinding,
116
120
  TupleExpr,
117
121
  Var,
122
+ WhileStmt,
118
123
  Zip,
119
124
  )
120
125
  from ..function import Function
121
126
  from ..types import ListType, TupleType, Type
122
127
  from ..utils import Unionfind
128
+ from .array_size import (
129
+ ArraySizeAnalysis,
130
+ ArraySizeBound,
131
+ ListSize,
132
+ TupleSize,
133
+ concrete_size,
134
+ )
123
135
  from .define_use import DefineUse, DefineUseAnalysis
124
- from .reaching_defs import AssignDef, Definition, PhiDef, same_object_defs
136
+ from .reaching_defs import AssignDef, Definition, same_object_defs
125
137
  from .type_infer import TypeAnalysis, TypeInfer
126
138
 
127
139
  if TYPE_CHECKING:
@@ -382,8 +394,18 @@ class _Regions:
382
394
  class AliasAnalysis:
383
395
  """Result of :class:`Alias`.
384
396
 
385
- ``sites`` lists every list allocation the function performs, plus one per
386
- list nested in each parameter's type.
397
+ ``written_regions``, ``slot_replaced`` and ``returned_levels`` are syntactic
398
+ facts keyed by region, for a consumer deciding how a list is held: whether a
399
+ reference may be ``const``, whether it may be a reference at all, and what
400
+ the ``return``s hand back together.
401
+
402
+ Attributes:
403
+ sites: every list allocation, plus one per nested parameter list.
404
+ written_regions: regions an ``xs[i] = e`` here stores into.
405
+ slot_replaced: element regions an ``xss[i] = <list>`` replaces.
406
+ returned_levels: regions the ``return``s hand back, by depth.
407
+ consumed_defs: per region, definitions whose value *moves* into a
408
+ container rather than coexisting with it.
387
409
  """
388
410
 
389
411
  sites: list[AllocSite]
@@ -391,6 +413,10 @@ class AliasAnalysis:
391
413
  _by_def: dict[Definition, Region]
392
414
  _by_expr: dict[Expr, Region]
393
415
  _site_region: dict[AllocSite, Region]
416
+ written_regions: set[Region] = field(default_factory=set)
417
+ slot_replaced: set[Region] = field(default_factory=set)
418
+ returned_levels: list[set[Region]] = field(default_factory=list)
419
+ consumed_defs: dict[Region, set[Definition]] = field(default_factory=dict)
394
420
 
395
421
  def region_of(self, d: Definition, depth: int = 0) -> Region | None:
396
422
  """What may be the same list as *d*, *depth* levels in.
@@ -442,6 +468,36 @@ class AliasAnalysis:
442
468
  names plus container slots."""
443
469
  return self._regions.referrers(region)
444
470
 
471
+ def consumed(self, region: Region) -> frozenset[Definition]:
472
+ """*region*'s definitions that hand their value to a container and are
473
+ never read again — see :attr:`consumed_defs`."""
474
+ return frozenset(self.consumed_defs.get(self._regions.find(region), ()))
475
+
476
+ def referrers_after_moves(self, region: Region) -> int:
477
+ """:meth:`referrers`, less the names the value *moves* out of.
478
+
479
+ Counting a consumed name makes ``xs = [n, n]; return (xs, 1.0)`` look
480
+ shared where the same program written inline does not.
481
+
482
+ A name is discounted only when *every* definition of it here is
483
+ consumed: :meth:`referrers` counts names, so one definition read twice
484
+ keeps the name a place however its siblings are used.
485
+
486
+ **A caller of this owes the move**, and only for a definition
487
+ :meth:`consumed` names. Moving one it does not is a use-after-move.
488
+ """
489
+ root = self._regions.find(region)
490
+ consumed = self.consumed(root)
491
+ if not consumed:
492
+ return self.referrers(root)
493
+ by_name: dict[NamedId, list[Definition]] = {}
494
+ for d in self.defs_in(root):
495
+ by_name.setdefault(d.name, []).append(d)
496
+ gone = sum(
497
+ 1 for ds in by_name.values() if all(d in consumed for d in ds)
498
+ )
499
+ return self.referrers(root) - gone
500
+
445
501
  def region_of_site(self, site: AllocSite) -> Region | None:
446
502
  """Which region *site*'s allocation lives in."""
447
503
  region = self._site_region.get(site)
@@ -493,7 +549,8 @@ class AliasAnalysis:
493
549
  self._regions.is_returned(region)
494
550
  and not self._regions.has_param_site(region)
495
551
  and not self._regions.is_shared_out(region)
496
- and not self.is_shared(site)
552
+ # a name the returned container consumed is not a second holder
553
+ and self.referrers_after_moves(region) <= 1
497
554
  )
498
555
 
499
556
  def is_uniquely_owned(self, site: AllocSite) -> bool:
@@ -565,10 +622,21 @@ class _Builder(DefaultVisitor):
565
622
  self._merge_redefinitions()
566
623
  self._seed_params()
567
624
  self._visit_function(self.func, None)
568
- return AliasAnalysis(
625
+ alias = AliasAnalysis(
569
626
  self.sites, self.regions, self.by_def, self._by_expr,
570
627
  self.site_region,
571
628
  )
629
+ # A second walk, because every fact asks `region_of` for where a place
630
+ # ends up and that is settled only once every merge has run. Inside
631
+ # `run` rather than beside it: `transfers_ownership` reads them, so a
632
+ # result without them would answer a different question.
633
+ facts = _RegionFacts(alias, self.def_use)
634
+ facts._visit_function(self.func, None)
635
+ alias.written_regions = facts.written
636
+ alias.slot_replaced = facts.slot_replaced
637
+ alias.returned_levels = facts.returned_levels
638
+ alias.consumed_defs = facts.consumed
639
+ return alias
572
640
 
573
641
  # -- regions --------------------------------------------------------------
574
642
 
@@ -849,6 +917,224 @@ class _Builder(DefaultVisitor):
849
917
  super()._visit_return(stmt, ctx)
850
918
 
851
919
 
920
+ class _RegionFacts(DefaultVisitor):
921
+ """Syntactic facts about regions, read off a *finished* region graph.
922
+
923
+ A second walk rather than part of :class:`_Builder`: each fact asks
924
+ ``region_of`` for the region a place ends up in, which is only settled once
925
+ every merge has run.
926
+ """
927
+
928
+ def __init__(self, alias: 'AliasAnalysis', def_use: DefineUseAnalysis):
929
+ self.alias = alias
930
+ self.def_use = def_use
931
+ self.written: set[Region] = set()
932
+ self.slot_replaced: set[Region] = set()
933
+ self.returned_levels: list[set[Region]] = []
934
+ self.consumed: dict[Region, set[Definition]] = {}
935
+ self._siblings: set[Stmt] = set()
936
+ self._repeats = 0
937
+ # Which phi consumed each operand. A phi is not a use site, so
938
+ # sole-use would otherwise hold for a name whose value flows out of its
939
+ # branch -- and a merge inside one object's own class, a loop head
940
+ # threading a list through its fills, is not such a flow.
941
+ self._phi_of: dict[Definition, set[Definition]] = {}
942
+ for ps in def_use.phis.values():
943
+ for p in ps:
944
+ for i in (p.lhs, p.rhs):
945
+ self._phi_of.setdefault(def_use.defs[i], set()).add(p)
946
+ for d in alias.all_defs():
947
+ if isinstance(d, AssignDef) and isinstance(d.site, IndexedAssign):
948
+ if (r := alias.region_of(d)) is not None:
949
+ self.written.add(r)
950
+
951
+ def _visit_block(self, block: StmtBlock, ctx):
952
+ outer, self._siblings = self._siblings, set(block.stmts)
953
+ for stmt in block.stmts:
954
+ self._visit_statement(stmt, ctx)
955
+ self._siblings = outer
956
+
957
+ def _visit_while(self, stmt: WhileStmt, ctx):
958
+ # the condition re-runs every iteration, in the enclosing block
959
+ self._repeats += 1
960
+ self._visit_expr(stmt.cond, ctx)
961
+ self._repeats -= 1
962
+ self._visit_block(stmt.body, ctx)
963
+
964
+ def _visit_list_comp(self, e: ListComp, ctx):
965
+ for iterable in e.iterables:
966
+ self._visit_expr(iterable, ctx)
967
+ # the element runs once per item, and is an expression rather than a
968
+ # block, so the block guard alone does not see the repetition
969
+ self._repeats += 1
970
+ self._visit_expr(e.elt, ctx)
971
+ self._repeats -= 1
972
+
973
+ def _visit_tuple_expr(self, e: TupleExpr, ctx):
974
+ self._note_consumed(e.elts)
975
+ super()._visit_tuple_expr(e, ctx)
976
+
977
+ def _visit_list_expr(self, e: ListExpr, ctx):
978
+ self._note_consumed(e.elts)
979
+ super()._visit_list_expr(e, ctx)
980
+
981
+ def _same_object(self, d: Definition) -> set[Definition]:
982
+ """*d* and every definition denoting the same runtime object.
983
+
984
+ ``same_object_defs`` points from a phi to its operands and from an
985
+ ``xs[i] = e`` to what it mutated, so the closure reaches the whole chain
986
+ that filled one list.
987
+ """
988
+ seen: set[Definition] = set()
989
+ stack = [d]
990
+ while stack:
991
+ cur = stack.pop()
992
+ if cur in seen:
993
+ continue
994
+ seen.add(cur)
995
+ stack.extend(self.def_use.defs[i] for i in same_object_defs(cur))
996
+ return seen
997
+
998
+ def _moves_out(self, cls: set[Definition], e: Var) -> bool:
999
+ """Whether the object *cls* denotes is read only at *e*.
1000
+
1001
+ A write-through is not a second read: ``xs[i] = v`` uses the name, but
1002
+ makes a definition ``same_object`` with the one it wrote through, so it
1003
+ is already in *cls*. The fill happens before the move.
1004
+ """
1005
+ for m in cls:
1006
+ for u in self.def_use.uses.get(m, ()):
1007
+ if u is e:
1008
+ continue
1009
+ if isinstance(u, IndexedAssign) and isinstance(u.var, NamedId):
1010
+ fresh = self.def_use.find_def_from_site(u.var, u)
1011
+ if fresh in cls:
1012
+ continue
1013
+ return False
1014
+ return True
1015
+
1016
+ def _note_consumed(self, elts: 'tuple[Expr, ...]') -> None:
1017
+ """Record each operand of a construction whose value *moves* into it.
1018
+
1019
+ Sound only where the construction provably runs once for that value, so
1020
+ every way it could run again is refused: a use that re-executes
1021
+ (``_repeats``), one whose definition is not a sibling statement, and one
1022
+ whose value leaves through a phi outside the same object.
1023
+
1024
+ The unit is the *object*, not one definition of it: a list filled by a
1025
+ loop reaches its consumer through a phi over the allocation and the
1026
+ stores, and all three are recorded, since
1027
+ :meth:`referrers_after_moves` drops a name only when every definition of
1028
+ it is consumed.
1029
+ """
1030
+ if self._repeats:
1031
+ return
1032
+ for e in elts:
1033
+ if not isinstance(e, Var):
1034
+ continue
1035
+ d = self.def_use.use_to_def.get(e)
1036
+ if d is None:
1037
+ continue
1038
+ cls = self._same_object(d)
1039
+ # the value has to be one this function made: a parameter is the
1040
+ # caller's storage, and a class with no `Assign` allocated nothing
1041
+ sites = [m.site for m in cls if isinstance(m, AssignDef)]
1042
+ allocs = [s for s in sites if isinstance(s, Assign)]
1043
+ if not allocs or any(not isinstance(s, (Assign, IndexedAssign))
1044
+ for s in sites):
1045
+ continue
1046
+ if not self._moves_out(cls, e):
1047
+ continue
1048
+ # a phi outside the class carries the value somewhere else; one
1049
+ # inside it is the loop head threading this same list
1050
+ if any(p not in cls
1051
+ for m in cls for p in self._phi_of.get(m, ())):
1052
+ continue
1053
+ if any(s not in self._siblings for s in allocs):
1054
+ continue
1055
+ if (r := self.alias.region_of(d)) is not None:
1056
+ self.consumed.setdefault(r, set()).update(cls)
1057
+
1058
+ def _visit_indexed_assign(self, stmt: IndexedAssign, ctx):
1059
+ # Only a store of a *list* replaces a slot: a scalar write goes through
1060
+ # the cell, leaving whatever region the element held intact.
1061
+ if (
1062
+ isinstance(stmt.var, NamedId)
1063
+ and self.alias.region_of_expr(stmt.expr) is not None
1064
+ ):
1065
+ d = self.def_use.find_def_from_site(stmt.var, stmt)
1066
+ if (r := self.alias.region_of(d, len(stmt.indices))) is not None:
1067
+ self.slot_replaced.add(r)
1068
+ super()._visit_indexed_assign(stmt, ctx)
1069
+
1070
+ def _visit_return(self, stmt: ReturnStmt, ctx):
1071
+ depth = 0
1072
+ while (r := self.alias.region_of_expr(stmt.expr, depth)) is not None:
1073
+ while len(self.returned_levels) <= depth:
1074
+ self.returned_levels.append(set())
1075
+ self.returned_levels[depth].add(r)
1076
+ depth += 1
1077
+ super()._visit_return(stmt, ctx)
1078
+
1079
+
1080
+ _ALLOC_EXPRS = (
1081
+ ListExpr, ListComp, Empty, ListSlice, Range1, Range2, Range3, Zip,
1082
+ Enumerate, Call,
1083
+ )
1084
+ """Expression forms that produce a list of their own.
1085
+
1086
+ These seed :func:`region_sizes` in addition to the defs: a returned literal has
1087
+ a region but no def, and only its ``by_expr`` bound can size it. Plain reads
1088
+ (``Var``, ``ListRef``) are left out -- they only mirror what a def contributed,
1089
+ and a lossy read-side bound must not poison a region a definition proved.
1090
+ """
1091
+
1092
+
1093
+ def region_sizes(
1094
+ alias: AliasAnalysis, array_size: ArraySizeAnalysis,
1095
+ ) -> dict[Region, int | None]:
1096
+ """One proven length per region, by meeting every contribution.
1097
+
1098
+ A region's storage must hold every value it is ever bound to, so the meet
1099
+ poisons on any doubt: a def with no bound, a level the size analysis
1100
+ answered ``None`` for, a symbolic size (a per-run variable, never a length
1101
+ a fixed-length representation can spell), or two differing lengths all force ``None``.
1102
+ Contributions walk each bound structurally against the region graph, so the
1103
+ table is keyed by region exactly where a consumer reads it.
1104
+
1105
+ A free function because an :class:`AliasAnalysis` is usable without an
1106
+ :class:`~fpy2.analysis.array_size.ArraySizeAnalysis`; only a caller wanting
1107
+ lengths needs one.
1108
+ """
1109
+ sizes: dict[Region, int | None] = {}
1110
+
1111
+ def contribute(region: Region, k: int | None) -> None:
1112
+ if region in sizes and sizes[region] != k:
1113
+ sizes[region] = None
1114
+ else:
1115
+ sizes[region] = k
1116
+
1117
+ def seed(bound: ArraySizeBound, region: Region | None) -> None:
1118
+ if region is None:
1119
+ return
1120
+ match bound:
1121
+ case ListSize():
1122
+ contribute(region, concrete_size(bound.size))
1123
+ seed(bound.elt, alias.region_at(region))
1124
+ case TupleSize():
1125
+ for i, b in enumerate(bound.elts):
1126
+ seed(b, alias.region_field(region, i))
1127
+ case None:
1128
+ contribute(region, None)
1129
+
1130
+ for d in alias.all_defs():
1131
+ seed(array_size.by_def.get(d), alias.region_of(d))
1132
+ for e, bound in array_size.by_expr.items():
1133
+ if isinstance(e, _ALLOC_EXPRS):
1134
+ seed(bound, alias.region_of_expr(e))
1135
+ return sizes
1136
+
1137
+
852
1138
  class Alias:
853
1139
  """Alias analysis for FPy programs."""
854
1140
 
@@ -23,7 +23,7 @@ from typing import TypeAlias
23
23
  from ..ast.fpyast import *
24
24
  from ..ast.visitor import DefaultVisitor
25
25
  from ..function import Function
26
- from ..number import INTEGER, REAL, Float
26
+ from ..number import INTEGER, REAL, Context, Float
27
27
  from ..types import ListType, TupleType, Type
28
28
  from ..utils import Gensym, NamedId, Unionfind
29
29
  from .context_use import ContextUse, ContextUseAnalysis, ContextUseSite
@@ -471,13 +471,9 @@ class _ArraySizeInferInstance(DefaultVisitor):
471
471
 
472
472
  acc: ArraySizeBound = elt_ty
473
473
  for arg in arg_rev:
474
- size_v = self._get_eval(arg)
475
- size = (
476
- int(INTEGER.round(size_v))
477
- if isinstance(size_v, Float | Fraction)
478
- else None
479
- )
480
- acc = ListSize(acc, size)
474
+ # `_const_int`, not `_get_eval`: a dimension is commonly
475
+ # `len(xs)`, which only the former folds
476
+ acc = ListSize(acc, self._const_int(arg))
481
477
  return acc
482
478
 
483
479
  case _:
@@ -579,6 +575,8 @@ class _ArraySizeInferInstance(DefaultVisitor):
579
575
  if c is not None:
580
576
  return (None, c)
581
577
 
578
+ # through a name: `t = i + 32; xs[i:t]` sizes like `xs[i:i+32]`
579
+ e = self.def_use.defining_expr(e)
582
580
  match e:
583
581
  case Add() if self._is_exact(e):
584
582
  c = self._const_int(e.second)
@@ -604,27 +602,39 @@ class _ArraySizeInferInstance(DefaultVisitor):
604
602
  ) -> int | None:
605
603
  """``hi - lo`` as a compile-time constant, or ``None``.
606
604
 
607
- Constant exactly when the two affine forms share a base — both
608
- pure constants, or the same symbolic base (so it cancels).
605
+ Constant exactly when the two affine forms share a base — both pure
606
+ constants, or one variable with one *definition*. Definitions, not
607
+ names: :meth:`_affine` follows a name to what it was assigned, so the two
608
+ bases may come from different program points, where a structural
609
+ comparison would match ``a + b`` against a later ``a + b`` that reads a
610
+ reassigned ``a``.
609
611
  """
610
612
  (lbase, loff), (hbase, hoff) = lo, hi
611
- same_base = (
612
- (lbase is None and hbase is None)
613
- or (lbase is not None and hbase is not None
614
- and lbase.is_equiv(hbase))
615
- )
616
- return hoff - loff if same_base else None
613
+ if lbase is None and hbase is None:
614
+ return hoff - loff
615
+ if not (isinstance(lbase, Var) and isinstance(hbase, Var)):
616
+ return None
617
+ ldef = self.def_use.find_def_from_use(lbase)
618
+ if ldef is not self.def_use.find_def_from_use(hbase):
619
+ return None
620
+ return hoff - loff
617
621
 
618
622
  def _const_int(self, e: Expr) -> int | None:
619
623
  """The integer value *e* statically evaluates to, or ``None``.
620
624
 
621
- Covers partial-eval constants and the integer-valued list queries
622
- ``len(xs)`` / ``size(xs, d)`` / ``dim(xs)`` when the queried size
623
- is statically known (``dim`` always is — it's the nesting depth).
625
+ Covers partial-eval constants, the integer-valued list queries
626
+ ``len(xs)`` / ``size(xs, d)`` / ``dim(xs)`` when the queried size is
627
+ statically known (``dim`` always is — it's the nesting depth), and
628
+ ``+`` / ``-`` / ``*`` over those. The arithmetic is what a lowered
629
+ multi-clause comprehension allocates with: ``fp.empty(len(xs) *
630
+ len(ys))``, whose size is otherwise unknown and whose list is then a
631
+ ``std::vector`` where the sizes are right there in the types.
624
632
  """
625
633
  val = self._get_eval(e)
626
634
  if isinstance(val, Float | Fraction):
627
635
  return int(INTEGER.round(val))
636
+ # through a name: a size query bound to one is still that query
637
+ e = self.def_use.defining_expr(e)
628
638
  match e:
629
639
  case Len():
630
640
  bound = self.by_expr.get(e.arg)
@@ -645,8 +655,38 @@ class _ArraySizeInferInstance(DefaultVisitor):
645
655
  bound = bound.elt
646
656
  if isinstance(bound, ListSize) and isinstance(bound.size, int):
647
657
  return bound.size
658
+ case Add() | Sub() | Mul():
659
+ lhs = self._const_int(e.first)
660
+ rhs = self._const_int(e.second)
661
+ if lhs is None or rhs is None:
662
+ return None
663
+ match e:
664
+ case Add():
665
+ folded = lhs + rhs
666
+ case Sub():
667
+ folded = lhs - rhs
668
+ case _:
669
+ folded = lhs * rhs
670
+ return folded if self._holds_int(e, folded) else None
648
671
  return None
649
672
 
673
+ def _holds_int(self, e: ContextUseSite, value: int) -> bool:
674
+ """Whether *e*'s rounding leaves the integer *value* alone.
675
+
676
+ :meth:`_is_exact` asks whether the operation rounds *at all*, which
677
+ takes a ``REAL`` scope; an integer result needs only that the active
678
+ context represent it. That is what carries a length through
679
+ ``integer_ctx``, which the loop rewrites wrap their index arithmetic in.
680
+
681
+ Checking the result alone is enough because the operands are
682
+ compile-time constants -- a static length, or a partial-eval value --
683
+ so nothing below *e* rounds.
684
+ """
685
+ scope = self._ctx_use.use_to_scope.get(e)
686
+ if scope is None or not isinstance(scope.ctx, Context):
687
+ return False
688
+ return scope.ctx.representable_under(Float.from_int(value, ctx=INTEGER))
689
+
650
690
  @staticmethod
651
691
  def _list_depth(bound: ArraySizeBound) -> int:
652
692
  """Number of nested list dimensions in *bound* (== ``dim``)."""
@@ -853,8 +893,11 @@ class _ArraySizeInferInstance(DefaultVisitor):
853
893
  def _len_size(self, e: Expr) -> ArraySize:
854
894
  """The size ``e`` constrains: the list's size for ``len(xs)``, the
855
895
  constant for an integer expression, else ``None`` (not relatable)."""
856
- if isinstance(e, Len):
857
- bound = self.by_expr.get(e.arg)
896
+ # through a name: `n = len(xs); assert n == 4` pins what
897
+ # `assert len(xs) == 4` does
898
+ src = self.def_use.defining_expr(e)
899
+ if isinstance(src, Len):
900
+ bound = self.by_expr.get(src.arg)
858
901
  return bound.size if isinstance(bound, ListSize) else None
859
902
  return self._const_int(e)
860
903
 
@@ -47,13 +47,7 @@ class _CallCollector(DefaultVisitor):
47
47
 
48
48
  def _visit_call(self, e: Call, ctx: None):
49
49
  self.calls.append(e)
50
- # ``DefaultVisitor._visit_call`` only walks positional args, so
51
- # visit keyword-argument values explicitly — a call nested in a
52
- # kwarg value (and any cycle through it) would otherwise be
53
- # missed from the graph and the acyclicity guard.
54
50
  super()._visit_call(e, ctx)
55
- for _, kwarg in e.kwargs:
56
- self._visit_expr(kwarg, ctx)
57
51
 
58
52
  def collect(self, func: FuncDef) -> list[Call]:
59
53
  self.calls = []
@@ -16,7 +16,7 @@ from typing import TypeAlias
16
16
  from ..ast.fpyast import *
17
17
  from ..ast.visitor import DefaultVisitor
18
18
  from ..fpc_context import FPCoreContext
19
- from ..number import Context
19
+ from ..number import REAL, Context
20
20
  from ..types import ContextParam
21
21
  from ..utils import Gensym, NamedId, default_repr
22
22
  from .define_use import DefineUse, DefineUseAnalysis
@@ -56,7 +56,15 @@ class ContextUseAnalysis:
56
56
  """all context scopes, ordered by introduction"""
57
57
 
58
58
  uses: dict[ContextScope, set[ContextUseSite]]
59
- """mapping from context scope to use sites"""
59
+ """mapping from context scope to use sites.
60
+
61
+ A key here need not be in :attr:`scopes`: **E-Context** evaluates a
62
+ ``with``'s context expression under ``REAL``, so its uses get a ``REAL``
63
+ scope that introduces no ``with`` in the program. Consumers keying scopes
64
+ by ``site`` (the cpp emitter, ``dead_code``) iterate :attr:`scopes` and so
65
+ never see it; consumers asking :meth:`find_scope_from_use` get the answer
66
+ the semantics give.
67
+ """
60
68
 
61
69
  use_to_scope: dict[ContextUseSite, ContextScope]
62
70
  """mapping from use site to context scope"""
@@ -148,25 +156,25 @@ class _ContextUseInstance(DefaultVisitor):
148
156
  self._record_use(e, scope)
149
157
 
150
158
  def _visit_call(self, e: Call, scope: ContextScope):
151
- for arg in e.args:
152
- self._visit_expr(arg, scope)
153
- for _, kwarg in e.kwargs:
154
- self._visit_expr(kwarg, scope)
159
+ super()._visit_call(e, scope)
155
160
  self._record_use(e, scope)
156
161
 
157
162
  # ------------------------------------------------------------------
158
163
  # Statement visitors
159
164
 
160
165
  def _visit_context(self, stmt: ContextStmt, scope: ContextScope):
161
- # The context expression is evaluated under real arithmetic (not the
162
- # enclosing floating-point context), so we do not visit it as a use
163
- # of the enclosing context. This is consistent with how
164
- # partial_eval evaluates context expressions under REAL.
165
166
  # Resolve the context expression, falling back to a symbolic
166
167
  # variable when partial evaluation cannot determine the value.
167
168
  ctx = self._resolve_ctx_expr(stmt.ctx)
168
169
  # Create a fresh context scope for the body.
169
170
  new_scope = self._make_scope(stmt, ctx)
171
+ # **E-Context** evaluates the context expression under `REAL`, not the
172
+ # enclosing context, so its uses get a scope of their own. Built after
173
+ # the body's, so a `with` whose context *is* `REAL` -- where the two
174
+ # compare equal -- merges into it rather than clearing it.
175
+ real_scope = ContextScope(stmt, REAL)
176
+ self.uses.setdefault(real_scope, set())
177
+ self._visit_expr(stmt.ctx, real_scope)
170
178
  self._visit_block(stmt.body, new_scope)
171
179
 
172
180
  # ------------------------------------------------------------------