fpy2 0.2.2__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.
- {fpy2-0.2.2 → fpy2-0.2.3}/PKG-INFO +1 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/__init__.py +13 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/__init__.py +8 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/alias.py +295 -9
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/array_size.py +64 -21
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/call_graph.py +0 -6
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/context_use.py +18 -10
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/define_use.py +30 -4
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/format_infer/__init__.py +16 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/format_infer/analysis.py +92 -25
- fpy2-0.2.3/fpy2/analysis/format_infer/double_round.py +288 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/format_infer/format.py +146 -60
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/live_vars.py +2 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/partial_eval.py +31 -38
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/purity.py +0 -4
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/reachability.py +20 -9
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/reaching_defs.py +22 -0
- fpy2-0.2.3/fpy2/analysis/storage_infer.py +332 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/syntax_check.py +55 -6
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/value_class.py +79 -5
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/ast/visitor.py +2 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/compiler.py +142 -32
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/emitter.py +308 -328
- fpy2-0.2.3/fpy2/backend/cpp/storage.py +269 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/unbox.py +62 -163
- fpy2-0.2.3/fpy2/backend/cpp/unfold_round.py +389 -0
- fpy2-0.2.2/fpy2/backend/cpp/storage_infer.py → fpy2-0.2.3/fpy2/backend/cpp/variables.py +119 -199
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/fpc.py +44 -34
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/function.py +62 -2
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/interpret/__init__.py +1 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/interpret/byte.py +66 -82
- fpy2-0.2.3/fpy2/interpret/value.py +174 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/__init__.py +1 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/context.py +21 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/efloat.py +18 -14
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/exponential.py +3 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/mp_fixed.py +9 -11
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/mp_float.py +163 -19
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/mpb_fixed.py +9 -10
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/mpb_float.py +168 -19
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/mps_float.py +171 -19
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/real.py +3 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/number/__init__.py +1 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/number/floats.py +17 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/number/reals.py +1 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/primitive.py +6 -22
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/rewrite/__init__.py +2 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/rewrite/applier.py +24 -29
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/rewrite/matcher.py +60 -102
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/rewrite/pattern.py +10 -0
- fpy2-0.2.3/fpy2/rewrite/rewrite.py +313 -0
- fpy2-0.2.3/fpy2/rewrite/search.py +120 -0
- fpy2-0.2.3/fpy2/strategies/__init__.py +86 -0
- fpy2-0.2.3/fpy2/strategies/anf.py +69 -0
- fpy2-0.2.3/fpy2/strategies/comp_lower.py +108 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/context_lift.py +1 -2
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/fixed_rescale.py +15 -12
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/float_lower.py +15 -12
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/free_var.py +1 -2
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/func_inline.py +13 -8
- fpy2-0.2.3/fpy2/strategies/hoistable.py +85 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/iter_elim.py +3 -0
- fpy2-0.2.3/fpy2/strategies/iter_unfold.py +161 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/loop_split.py +9 -8
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/loop_unroll.py +19 -14
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/mono.py +1 -2
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/neg_zero_unfold.py +15 -12
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/overflow_unfold.py +15 -12
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/reduce_fusion.py +3 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/round_elim.py +3 -0
- fpy2-0.2.3/fpy2/strategies/round_insert.py +108 -0
- fpy2-0.2.3/fpy2/strategies/round_split.py +123 -0
- fpy2-0.2.3/fpy2/strategies/simple.py +63 -0
- fpy2-0.2.3/fpy2/strategies/sites.py +224 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/strategies/special_unfold.py +15 -12
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/__init__.py +26 -0
- fpy2-0.2.3/fpy2/transform/anf.py +481 -0
- fpy2-0.2.3/fpy2/transform/comp_to_loop.py +558 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/const_fold.py +14 -3
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/copy_propagate.py +2 -2
- fpy2-0.2.3/fpy2/transform/cursor.py +440 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/dead_code.py +112 -25
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/error.py +6 -5
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/float_to_fixed.py +50 -8
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/for_unroll.py +129 -41
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/free_var_elim.py +13 -2
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/func_inline.py +137 -56
- fpy2-0.2.3/fpy2/transform/hoistable.py +584 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/iter_elim.py +1 -6
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/lift_context.py +18 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/monomorphize.py +19 -3
- fpy2-0.2.3/fpy2/transform/path.py +339 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/rescale_fixed.py +46 -8
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/round_elim.py +28 -33
- fpy2-0.2.3/fpy2/transform/round_insert.py +189 -0
- fpy2-0.2.3/fpy2/transform/simplify.py +88 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/specialize.py +6 -4
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/split_loop.py +132 -41
- fpy2-0.2.3/fpy2/transform/split_round.py +395 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/subst_var.py +20 -1
- fpy2-0.2.3/fpy2/transform/unfold_iter.py +242 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/unfold_neg_zero.py +79 -38
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/unfold_overflow.py +61 -15
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/unfold_special.py +114 -63
- fpy2-0.2.3/fpy2/transform/utils.py +809 -0
- fpy2-0.2.3/fpy2/transform/while_unroll.py +117 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/decorator.py +8 -5
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2.egg-info/PKG-INFO +1 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2.egg-info/SOURCES.txt +22 -1
- {fpy2-0.2.2 → fpy2-0.2.3}/pyproject.toml +1 -1
- fpy2-0.2.2/fpy2/backend/cpp/storage.py +0 -266
- fpy2-0.2.2/fpy2/rewrite/rewrite.py +0 -235
- fpy2-0.2.2/fpy2/strategies/__init__.py +0 -42
- fpy2-0.2.2/fpy2/strategies/simple.py +0 -73
- fpy2-0.2.2/fpy2/transform/utils.py +0 -312
- fpy2-0.2.2/fpy2/transform/while_unroll.py +0 -111
- {fpy2-0.2.2 → fpy2-0.2.3}/LICENSE +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/README.md +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/defs.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/escape.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/analysis/type_infer.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/ast/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/ast/formatter.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/ast/fpyast.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/backend.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/ops.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/target.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/types.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/backend/cpp/utils.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/decorator.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/env.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/fpc_context.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/frontend/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/frontend/fpc.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/frontend/parser.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/interpret/interpreter.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/base.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/core.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/eft.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/matrix.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/metrics.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/libraries/vector.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/module.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/fixed.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/format.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/ieee754.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/context/sm_fixed.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/engine/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/engine/engine.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/engine/gmp.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/engine/real.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/format.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/globals.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/gmputils.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/native.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/number/flags.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/number/round.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/ops.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/rewrite/subst.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/runner.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/enumerate_elim.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/for_bundling.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/for_unpack.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/if_bundling.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/reduce_fusion.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/rename_target.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/simplify_if.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/while_bundling.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/transform/zip_elim.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/types.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/__init__.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/bits.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/compare.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/default.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/float_params.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/fractions.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/gensym.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/identifier.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/inspect.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/iterator.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/loader.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/location.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/ordering.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/string.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/uninit.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2/utils/unionfind.py +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2.egg-info/dependency_links.txt +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2.egg-info/requires.txt +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/fpy2.egg-info/top_level.txt +0 -0
- {fpy2-0.2.2 → fpy2-0.2.3}/setup.cfg +0 -0
|
@@ -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
|
|
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,6 +35,13 @@ 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
|
|
40
47
|
from .value_class import (
|
|
@@ -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
|
|
84
|
-
|
|
85
|
-
|
|
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,
|
|
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
|
-
``
|
|
386
|
-
list
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
)
|
|
616
|
-
|
|
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
|
|
622
|
-
``len(xs)`` / ``size(xs, d)`` / ``dim(xs)`` when the queried size
|
|
623
|
-
|
|
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
|
-
|
|
857
|
-
|
|
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
|
-
|
|
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
|
# ------------------------------------------------------------------
|