frame-security 1.0.0__py3-none-any.whl

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 (236) hide show
  1. frame/__init__.py +61 -0
  2. frame/__main__.py +17 -0
  3. frame/analysis/__init__.py +14 -0
  4. frame/analysis/footprint.py +334 -0
  5. frame/analysis/formula.py +457 -0
  6. frame/analysis/predicate_matching.py +309 -0
  7. frame/analysis/unification.py +339 -0
  8. frame/arithmetic/__init__.py +9 -0
  9. frame/arithmetic/check.py +213 -0
  10. frame/arithmetic/synth.py +294 -0
  11. frame/checking/__init__.py +9 -0
  12. frame/checking/_checker_core.py +532 -0
  13. frame/checking/_disjunct_handling.py +78 -0
  14. frame/checking/_formula_helpers.py +456 -0
  15. frame/checking/_frame_inference.py +299 -0
  16. frame/checking/_ls_heuristics.py +262 -0
  17. frame/checking/_satisfiability_check.py +152 -0
  18. frame/checking/_unification_matching.py +145 -0
  19. frame/checking/checker.py +176 -0
  20. frame/checking/footprint_check.py +977 -0
  21. frame/checking/frame_inference.py +417 -0
  22. frame/checking/heuristics.py +264 -0
  23. frame/checking/incorrectness.py +484 -0
  24. frame/checking/s2s_normalized.py +373 -0
  25. frame/cli.py +1309 -0
  26. frame/core/__init__.py +10 -0
  27. frame/core/_ast_arrays_bitvecs.py +302 -0
  28. frame/core/_ast_base.py +44 -0
  29. frame/core/_ast_errors.py +137 -0
  30. frame/core/_ast_expressions.py +78 -0
  31. frame/core/_ast_pure.py +189 -0
  32. frame/core/_ast_quantifiers.py +66 -0
  33. frame/core/_ast_security.py +94 -0
  34. frame/core/_ast_spatial.py +144 -0
  35. frame/core/_ast_strings.py +150 -0
  36. frame/core/_lexer.py +131 -0
  37. frame/core/_parser_helpers.py +434 -0
  38. frame/core/ast.py +97 -0
  39. frame/core/parser.py +487 -0
  40. frame/encoding/__init__.py +8 -0
  41. frame/encoding/_bitvec.py +164 -0
  42. frame/encoding/_pure.py +279 -0
  43. frame/encoding/_pure_core.py +544 -0
  44. frame/encoding/_spatial.py +105 -0
  45. frame/encoding/_spatial_core.py +562 -0
  46. frame/encoding/_string.py +70 -0
  47. frame/encoding/_wand.py +426 -0
  48. frame/encoding/_wand_sat_encoding.py +356 -0
  49. frame/encoding/_wand_utils.py +221 -0
  50. frame/encoding/debug_dump.py +37 -0
  51. frame/encoding/encoder.py +374 -0
  52. frame/encoding/regex_parser.py +263 -0
  53. frame/folding/__init__.py +29 -0
  54. frame/folding/_goal_directed_helpers.py +236 -0
  55. frame/folding/apply.py +255 -0
  56. frame/folding/blind.py +272 -0
  57. frame/folding/bud.py +197 -0
  58. frame/folding/cyclic_unfold.py +413 -0
  59. frame/folding/goal_directed.py +603 -0
  60. frame/folding/hierarchical.py +172 -0
  61. frame/folding/utils.py +83 -0
  62. frame/folding/verify.py +699 -0
  63. frame/heap/__init__.py +11 -0
  64. frame/heap/_fold_proposals.py +645 -0
  65. frame/heap/_heap_types.py +97 -0
  66. frame/heap/graph.py +469 -0
  67. frame/heap/graph_analysis.py +605 -0
  68. frame/lemmas/__init__.py +9 -0
  69. frame/lemmas/_lemma_application.py +1274 -0
  70. frame/lemmas/_matcher.py +260 -0
  71. frame/lemmas/_substitution.py +240 -0
  72. frame/lemmas/base.py +831 -0
  73. frame/lemmas/compositional.py +302 -0
  74. frame/lemmas/dll_lemmas.py +541 -0
  75. frame/lemmas/list_lemmas.py +536 -0
  76. frame/lemmas/other_lemmas.py +328 -0
  77. frame/lemmas/taint_lemmas.py +317 -0
  78. frame/lemmas/validator.py +288 -0
  79. frame/lemmas/wand_lemmas.py +139 -0
  80. frame/predicates/__init__.py +91 -0
  81. frame/predicates/base.py +246 -0
  82. frame/predicates/base_registry.py +459 -0
  83. frame/predicates/dll_predicates.py +60 -0
  84. frame/predicates/list_predicates.py +373 -0
  85. frame/predicates/lsso_predicates.py +48 -0
  86. frame/predicates/parsed.py +305 -0
  87. frame/predicates/registry.py +402 -0
  88. frame/predicates/skiplist_predicates.py +128 -0
  89. frame/predicates/sorted_predicates.py +60 -0
  90. frame/predicates/tree_predicates.py +88 -0
  91. frame/preprocessing/__init__.py +8 -0
  92. frame/preprocessing/equality.py +340 -0
  93. frame/sil/__init__.py +117 -0
  94. frame/sil/analyzers/__init__.py +44 -0
  95. frame/sil/analyzers/csharp_interprocedural_taint.py +912 -0
  96. frame/sil/analyzers/function_summary.py +170 -0
  97. frame/sil/analyzers/interprocedural_analyzer.py +4109 -0
  98. frame/sil/analyzers/interprocedural_taint.py +889 -0
  99. frame/sil/analyzers/memory_safety.py +309 -0
  100. frame/sil/analyzers/path_sensitive_analyzer.py +861 -0
  101. frame/sil/analyzers/semantic_patterns.py +658 -0
  102. frame/sil/analyzers/sl_memory_analyzer.py +614 -0
  103. frame/sil/analyzers/sl_semantic_analyzer.py +1429 -0
  104. frame/sil/analyzers/ts_function_summarizer.py +818 -0
  105. frame/sil/changes.py +45 -0
  106. frame/sil/cli.py +662 -0
  107. frame/sil/cwe_taxonomy.py +223 -0
  108. frame/sil/fingerprints.py +266 -0
  109. frame/sil/framework_evidence.py +36 -0
  110. frame/sil/frontends/__init__.py +68 -0
  111. frame/sil/frontends/_literal_fields.py +38 -0
  112. frame/sil/frontends/_python_types.py +273 -0
  113. frame/sil/frontends/c_frontend.py +2103 -0
  114. frame/sil/frontends/csharp_frontend.py +2359 -0
  115. frame/sil/frontends/java_frontend.py +3258 -0
  116. frame/sil/frontends/javascript_frontend.py +3782 -0
  117. frame/sil/frontends/python_frontend.py +2323 -0
  118. frame/sil/hardening.py +128 -0
  119. frame/sil/instructions.py +773 -0
  120. frame/sil/llm_agent.py +233 -0
  121. frame/sil/llm_client.py +337 -0
  122. frame/sil/llm_detect.py +474 -0
  123. frame/sil/llm_exploit.py +252 -0
  124. frame/sil/llm_fix.py +150 -0
  125. frame/sil/llm_triage.py +244 -0
  126. frame/sil/loop_exit.py +70 -0
  127. frame/sil/metadata.py +397 -0
  128. frame/sil/packs/__init__.py +12 -0
  129. frame/sil/packs/apply.py +386 -0
  130. frame/sil/packs/cli.py +59 -0
  131. frame/sil/packs/data/c/civetweb.toml +42 -0
  132. frame/sil/packs/data/c/fastcgi.toml +34 -0
  133. frame/sil/packs/data/c/libarchive.toml +22 -0
  134. frame/sil/packs/data/c/libcurl.toml +23 -0
  135. frame/sil/packs/data/c/libmicrohttpd.toml +46 -0
  136. frame/sil/packs/data/c/libxml2.toml +25 -0
  137. frame/sil/packs/data/c/mongoose.toml +39 -0
  138. frame/sil/packs/data/c/openssl.toml +42 -0
  139. frame/sil/packs/data/cpp/boost-asio-beast.toml +37 -0
  140. frame/sil/packs/data/cpp/cpp-httplib.toml +54 -0
  141. frame/sil/packs/data/cpp/crow.toml +37 -0
  142. frame/sil/packs/data/cpp/grpc.toml +22 -0
  143. frame/sil/packs/data/cpp/qt.toml +83 -0
  144. frame/sil/packs/data/cpp/sql-clients.toml +43 -0
  145. frame/sil/packs/data/csharp/aspnet-classic.toml +45 -0
  146. frame/sil/packs/data/csharp/aspnetcore-minimal.toml +19 -0
  147. frame/sil/packs/data/csharp/aspnetcore.toml +104 -0
  148. frame/sil/packs/data/csharp/azure-functions.toml +29 -0
  149. frame/sil/packs/data/csharp/dapper.toml +19 -0
  150. frame/sil/packs/data/csharp/dotnet-system.toml +53 -0
  151. frame/sil/packs/data/csharp/entity-framework.toml +12 -0
  152. frame/sil/packs/data/csharp/entity-framework6.toml +11 -0
  153. frame/sil/packs/data/csharp/grpc.toml +16 -0
  154. frame/sil/packs/data/csharp/nhibernate.toml +10 -0
  155. frame/sil/packs/data/csharp/wcf.toml +24 -0
  156. frame/sil/packs/data/java/deserialization.toml +21 -0
  157. frame/sil/packs/data/java/hibernate-jpa.toml +24 -0
  158. frame/sil/packs/data/java/http-clients.toml +43 -0
  159. frame/sil/packs/data/java/javalin.toml +26 -0
  160. frame/sil/packs/data/java/jaxrs.toml +45 -0
  161. frame/sil/packs/data/java/jdk-core.toml +14 -0
  162. frame/sil/packs/data/java/jooq.toml +24 -0
  163. frame/sil/packs/data/java/jsf.toml +47 -0
  164. frame/sil/packs/data/java/micronaut.toml +38 -0
  165. frame/sil/packs/data/java/play.toml +29 -0
  166. frame/sil/packs/data/java/sparkjava.toml +32 -0
  167. frame/sil/packs/data/java/spring.toml +98 -0
  168. frame/sil/packs/data/java/struts2.toml +36 -0
  169. frame/sil/packs/data/java/template-engines.toml +66 -0
  170. frame/sil/packs/data/java/vertx-web.toml +61 -0
  171. frame/sil/packs/data/javascript/angular.toml +32 -0
  172. frame/sil/packs/data/javascript/aws-lambda.toml +16 -0
  173. frame/sil/packs/data/javascript/electron.toml +39 -0
  174. frame/sil/packs/data/javascript/fastify.toml +37 -0
  175. frame/sil/packs/data/javascript/graphql.toml +16 -0
  176. frame/sil/packs/data/javascript/hapi.toml +34 -0
  177. frame/sil/packs/data/javascript/http-clients.toml +22 -0
  178. frame/sil/packs/data/javascript/koa.toml +31 -0
  179. frame/sil/packs/data/javascript/nextjs.toml +59 -0
  180. frame/sil/packs/data/javascript/node-core.toml +53 -0
  181. frame/sil/packs/data/javascript/react-dom.toml +20 -0
  182. frame/sil/packs/data/javascript/redis.toml +11 -0
  183. frame/sil/packs/data/javascript/restify.toml +11 -0
  184. frame/sil/packs/data/javascript/sails.toml +15 -0
  185. frame/sil/packs/data/javascript/sanitizers.toml +51 -0
  186. frame/sil/packs/data/javascript/socketio.toml +13 -0
  187. frame/sil/packs/data/javascript/sql-drivers.toml +38 -0
  188. frame/sil/packs/data/javascript/template-engines.toml +18 -0
  189. frame/sil/packs/data/javascript/typeorm.toml +22 -0
  190. frame/sil/packs/data/python/aiohttp.toml +52 -0
  191. frame/sil/packs/data/python/asyncpg.toml +19 -0
  192. frame/sil/packs/data/python/bottle.toml +32 -0
  193. frame/sil/packs/data/python/builtins.toml +15 -0
  194. frame/sil/packs/data/python/cherrypy.toml +30 -0
  195. frame/sil/packs/data/python/django-rest-framework.toml +30 -0
  196. frame/sil/packs/data/python/django.toml +52 -0
  197. frame/sil/packs/data/python/falcon.toml +27 -0
  198. frame/sil/packs/data/python/fastapi.toml +56 -0
  199. frame/sil/packs/data/python/flask-extensions.toml +28 -0
  200. frame/sil/packs/data/python/http-clients.toml +32 -0
  201. frame/sil/packs/data/python/process-execution.toml +53 -0
  202. frame/sil/packs/data/python/pymongo.toml +30 -0
  203. frame/sil/packs/data/python/pyramid.toml +43 -0
  204. frame/sil/packs/data/python/sanic.toml +24 -0
  205. frame/sil/packs/data/python/sql-libraries.toml +47 -0
  206. frame/sil/packs/data/python/templates.toml +34 -0
  207. frame/sil/packs/data/python/tornado.toml +31 -0
  208. frame/sil/packs/loader.py +95 -0
  209. frame/sil/packs/schema.py +262 -0
  210. frame/sil/procedure.py +1009 -0
  211. frame/sil/project.py +1453 -0
  212. frame/sil/scanner.py +2605 -0
  213. frame/sil/source_facts.py +47 -0
  214. frame/sil/specs/__init__.py +57 -0
  215. frame/sil/specs/c_specs.py +862 -0
  216. frame/sil/specs/csharp_specs.py +825 -0
  217. frame/sil/specs/java_specs.py +1130 -0
  218. frame/sil/specs/javascript_specs.py +693 -0
  219. frame/sil/specs/python_specs.py +1199 -0
  220. frame/sil/suppressions.py +187 -0
  221. frame/sil/templates.py +248 -0
  222. frame/sil/translator.py +8003 -0
  223. frame/sil/types.py +470 -0
  224. frame/sil/verify.py +186 -0
  225. frame/utils/__init__.py +12 -0
  226. frame/utils/_normalization.py +341 -0
  227. frame/utils/formula_utils.py +78 -0
  228. frame/utils/frame_rule.py +398 -0
  229. frame/utils/proof_state.py +168 -0
  230. frame/utils/satisfiability.py +738 -0
  231. frame_security-1.0.0.dist-info/METADATA +157 -0
  232. frame_security-1.0.0.dist-info/RECORD +236 -0
  233. frame_security-1.0.0.dist-info/WHEEL +5 -0
  234. frame_security-1.0.0.dist-info/entry_points.txt +2 -0
  235. frame_security-1.0.0.dist-info/licenses/LICENSE +201 -0
  236. frame_security-1.0.0.dist-info/top_level.txt +1 -0
frame/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Separation Logic Entailment Checker using Z3
3
+
4
+ A Python library for checking entailments in separation logic,
5
+ supporting basic assertions and inductive predicates for data structures.
6
+
7
+ The library is organized into logical modules:
8
+ - core: AST and parser
9
+ - encoding: Z3 SMT encoding
10
+ - checking: Entailment checking and heuristics
11
+ - analysis: Formula analysis and reasoning
12
+ - heap: Heap graph and pattern detection
13
+ - folding: Predicate folding/unfolding
14
+ - arithmetic: Arithmetic reasoning
15
+ - preprocessing: Formula preprocessing
16
+ - predicates: Inductive predicate definitions
17
+ - lemmas: Lemma library
18
+ - utils: Utilities and proof management
19
+ """
20
+
21
+ # Core abstractions
22
+ from frame.core.ast import (
23
+ Formula, Emp, PointsTo, SepConj, Wand, And, Or, Not, Eq, Neq,
24
+ Lt, Le, Gt, Ge, True_, False_, Exists, Forall, Var, Const, PredicateCall,
25
+ # String expressions
26
+ StrLiteral, StrConcat, StrLen, StrSubstr, StrContains, StrMatches,
27
+ # Security and taint tracking
28
+ Taint, Sanitized, Source, Sink,
29
+ # Error states for incorrectness logic
30
+ Error, NullDeref, UseAfterFree, BufferOverflow
31
+ )
32
+ from frame.core.parser import parse, parse_entailment
33
+
34
+ # Main checker
35
+ from frame.checking.checker import EntailmentChecker
36
+
37
+ # Predicates
38
+ from frame.predicates import (
39
+ PredicateRegistry, InductivePredicate, PredicateValidationError,
40
+ ListSegment, Tree, GenericPredicate
41
+ )
42
+
43
+ __version__ = "1.0.0"
44
+ __all__ = [
45
+ # Core formulas and expressions
46
+ "Formula", "Emp", "PointsTo", "SepConj", "Wand", "And", "Or", "Not",
47
+ "Eq", "Neq", "Lt", "Le", "Gt", "Ge", "True_", "False_", "Exists", "Forall", "Var", "Const",
48
+ "PredicateCall",
49
+ # String expressions
50
+ "StrLiteral", "StrConcat", "StrLen", "StrSubstr", "StrContains", "StrMatches",
51
+ # Security and taint
52
+ "Taint", "Sanitized", "Source", "Sink",
53
+ # Error states
54
+ "Error", "NullDeref", "UseAfterFree", "BufferOverflow",
55
+ # Checker and predicates
56
+ "EntailmentChecker", "PredicateRegistry",
57
+ "InductivePredicate", "PredicateValidationError",
58
+ "ListSegment", "Tree", "GenericPredicate",
59
+ # Parser
60
+ "parse", "parse_entailment"
61
+ ]
frame/__main__.py ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Frame CLI entry point for `python -m frame`.
4
+
5
+ Usage:
6
+ python -m frame scan app.py
7
+ python -m frame solve "x |-> 5 |- x |-> 5"
8
+ python -m frame check entailments.txt
9
+ python -m frame parse "x |-> 5 * y |-> 3"
10
+ python -m frame repl
11
+ """
12
+
13
+ import sys
14
+ from frame.cli import main
15
+
16
+ if __name__ == "__main__":
17
+ sys.exit(main())
@@ -0,0 +1,14 @@
1
+ """
2
+ Formula analysis and reasoning utilities.
3
+
4
+ This module provides various analysis capabilities:
5
+ - Formula structure analysis
6
+ - Unification algorithm
7
+ - Predicate pattern matching
8
+ - Footprint analysis
9
+ """
10
+
11
+ from frame.analysis.formula import FormulaAnalyzer
12
+ from frame.analysis.unification import Unifier
13
+ from frame.analysis.predicate_matching import PredicateMatcher
14
+ from frame.analysis.footprint import FootprintAnalyzer
@@ -0,0 +1,334 @@
1
+ """
2
+ Footprint analysis for separation logic formulas.
3
+
4
+ A footprint is the set of heap locations (variables) that a formula may allocate or access.
5
+ This is used for sound affine weakening in the frame rule.
6
+
7
+ Key principles:
8
+ - Allocated locations are always in footprint
9
+ - Variable values (potential pointers) are in footprint
10
+ - Constant values are NOT in footprint
11
+ - Inductive predicates use one-step unfolding for precision
12
+ """
13
+
14
+ from typing import Set, List, Tuple
15
+ from frame.core.ast import (
16
+ Formula, Expr, Var, Const, Emp, PointsTo, SepConj, And, Or, Not,
17
+ PredicateCall, Exists, Forall, Wand
18
+ )
19
+
20
+
21
+ class FootprintAnalyzer:
22
+ """
23
+ Analyzes footprints (allocated heap locations + stored pointers) of separation logic formulas.
24
+
25
+ Uses one-step unfolding of inductive predicates for sound and precise footprint tracking.
26
+ Conservative: includes symbolic values that could alias other heap cells.
27
+ """
28
+
29
+ def __init__(self, unfold_depth=1):
30
+ """
31
+ Args:
32
+ unfold_depth: Depth of unfolding for inductive predicates (default: 1)
33
+ """
34
+ self.unfold_depth = unfold_depth
35
+ self._predicate_registry = None # Will be set by EntailmentChecker if available
36
+
37
+ def _is_constant(self, expr: Expr) -> bool:
38
+ """Check if expression is a constant (not a potential pointer)"""
39
+ if isinstance(expr, Const):
40
+ return True
41
+ if isinstance(expr, Var):
42
+ # Check if variable name looks like a constant (e.g., "nil", or numeric)
43
+ if expr.name in ["nil", "null"]:
44
+ return True
45
+ try:
46
+ int(expr.name) # Numeric string like "5"
47
+ return True
48
+ except ValueError:
49
+ return False # Regular variable name
50
+ return False
51
+
52
+ def footprint(self, formula: Formula) -> Set[str]:
53
+ """
54
+ Compute the footprint of a formula.
55
+
56
+ Footprint includes:
57
+ - All allocated locations (addresses in x |-> y)
58
+ - All variable values that could be pointers (y in x |-> y)
59
+ - Symbolic next values from unfolded predicates
60
+
61
+ Returns:
62
+ Set of variable names that may be allocated or referenced by this formula.
63
+ """
64
+ if isinstance(formula, Emp):
65
+ return set()
66
+
67
+ elif isinstance(formula, PointsTo):
68
+ # x |-> y allocates location x
69
+ fp = set()
70
+ if isinstance(formula.location, Var):
71
+ fp.add(formula.location.name)
72
+
73
+ # Include values that are potential pointers (variables, not constants)
74
+ # x |-> y → include y (variable, might be pointer)
75
+ # x |-> 5 → don't include 5 (constant, not a pointer)
76
+ for val in formula.values:
77
+ if isinstance(val, Var) and not self._is_constant(val):
78
+ fp.add(val.name)
79
+
80
+ return fp
81
+
82
+ elif isinstance(formula, PredicateCall):
83
+ # One-step unfolding for precision
84
+ # list(z) unfolds to: z |-> next * list(next)
85
+ # Footprint: {z, z_next} where z_next is symbolic
86
+ fp = set()
87
+
88
+ # Always include root pointer
89
+ for arg in formula.args:
90
+ if isinstance(arg, Var):
91
+ fp.add(arg.name)
92
+
93
+ # Add symbolic next values (conservative: any variable from predicate might store pointers)
94
+ # This captures that list(z) might contain any heap value
95
+ if self.unfold_depth >= 1:
96
+ # For predicates, add symbolic "next" values
97
+ # This conservatively assumes the predicate stores pointers
98
+ for arg in formula.args:
99
+ if isinstance(arg, Var):
100
+ # Add a symbolic next value for this root
101
+ fp.add(f"{arg.name}_next")
102
+
103
+ return fp
104
+
105
+ elif isinstance(formula, SepConj):
106
+ # P * Q: union of footprints
107
+ left_fp = self.footprint(formula.left)
108
+ right_fp = self.footprint(formula.right)
109
+ return left_fp.union(right_fp)
110
+
111
+ elif isinstance(formula, And):
112
+ # P & Q: union of spatial footprints
113
+ left_fp = self.footprint(formula.left)
114
+ right_fp = self.footprint(formula.right)
115
+ return left_fp.union(right_fp)
116
+
117
+ elif isinstance(formula, Or):
118
+ # P | Q: union (overapproximation - may allocate either)
119
+ left_fp = self.footprint(formula.left)
120
+ right_fp = self.footprint(formula.right)
121
+ return left_fp.union(right_fp)
122
+
123
+ elif isinstance(formula, Not):
124
+ # ¬P: no allocation in classical logic
125
+ # (negation is typically pure)
126
+ return set()
127
+
128
+ elif isinstance(formula, (Exists, Forall)):
129
+ # Quantifiers: footprint of body
130
+ return self.footprint(formula.formula)
131
+
132
+ elif isinstance(formula, Wand):
133
+ # P -* Q: footprint of Q (what's allocated when P is available)
134
+ # Conservative: union of both
135
+ left_fp = self.footprint(formula.left)
136
+ right_fp = self.footprint(formula.right)
137
+ return left_fp.union(right_fp)
138
+
139
+ else:
140
+ # Pure formulas, True, False, etc.
141
+ return set()
142
+
143
+ def _get_root_vars(self, formula: Formula) -> Set[str]:
144
+ """Extract root variables (allocated locations) from a formula"""
145
+ roots = set()
146
+ if isinstance(formula, PointsTo):
147
+ if isinstance(formula.location, Var):
148
+ roots.add(formula.location.name)
149
+ elif isinstance(formula, PredicateCall):
150
+ # For predicates, the first argument is typically the root
151
+ if formula.args and isinstance(formula.args[0], Var):
152
+ roots.add(formula.args[0].name)
153
+ elif isinstance(formula, SepConj):
154
+ roots.update(self._get_root_vars(formula.left))
155
+ roots.update(self._get_root_vars(formula.right))
156
+ return roots
157
+
158
+ def _contains_predicates(self, formula: Formula) -> bool:
159
+ """Check if formula contains any predicate calls"""
160
+ if isinstance(formula, PredicateCall):
161
+ return True
162
+ elif isinstance(formula, SepConj):
163
+ return self._contains_predicates(formula.left) or self._contains_predicates(formula.right)
164
+ elif isinstance(formula, And):
165
+ return self._contains_predicates(formula.left) or self._contains_predicates(formula.right)
166
+ return False
167
+
168
+ def _get_symbolic_values(self, formula: Formula) -> Set[str]:
169
+ """
170
+ Extract symbolic values (variables, not constants) stored in cells.
171
+ For x |-> y: returns {y} if y is variable, {} if y is constant
172
+ For x |-> 5: returns {}
173
+ """
174
+ values = set()
175
+ if isinstance(formula, PointsTo):
176
+ for val in formula.values:
177
+ if isinstance(val, Var) and not self._is_constant(val):
178
+ values.add(val.name)
179
+ elif isinstance(formula, PredicateCall):
180
+ # Predicates have symbolic _next values from one-step unfolding
181
+ # These are handled in footprint(), not here
182
+ pass
183
+ elif isinstance(formula, SepConj):
184
+ values.update(self._get_symbolic_values(formula.left))
185
+ values.update(self._get_symbolic_values(formula.right))
186
+ return values
187
+
188
+ def _flatten_sepconj(self, formula: Formula, start_pos: int = 0) -> List[Tuple[Formula, int]]:
189
+ """
190
+ Flatten a SepConj structure to assign positions to each conjunct.
191
+ Returns list of (formula, position) tuples.
192
+
193
+ Left-most formulas get lower positions (earlier in * order).
194
+ Example: (A * B) * C → [(A, 0), (B, 1), (C, 2)]
195
+ """
196
+ if isinstance(formula, SepConj):
197
+ # Recursively flatten left and right
198
+ left_flat = self._flatten_sepconj(formula.left, start_pos)
199
+ next_pos = start_pos + len(left_flat)
200
+ right_flat = self._flatten_sepconj(formula.right, next_pos)
201
+ return left_flat + right_flat
202
+ else:
203
+ # Base case: single formula
204
+ return [(formula, start_pos)]
205
+
206
+ def can_drop_safely_order_aware(self, remainder: Formula, remainder_pos: int,
207
+ other: Formula, other_pos: int) -> bool:
208
+ """
209
+ Check if remainder can be safely dropped using order-aware heuristic.
210
+
211
+ Order-aware affine SL heuristic:
212
+ - Remainder AFTER kept predicate (higher position) → safe to drop if disjoint
213
+ - Remainder BEFORE kept predicate (lower position) → conservative, block if has symbolic values
214
+
215
+ This captures: earlier cells may be referenced by later predicates,
216
+ but cells added later are safe to drop.
217
+
218
+ Args:
219
+ remainder: Formula to drop
220
+ remainder_pos: Position in * structure (lower = earlier)
221
+ other: Formula to keep
222
+ other_pos: Position in * structure
223
+
224
+ Returns:
225
+ True if safe to drop, False otherwise
226
+ """
227
+ fp_remainder = self.footprint(remainder)
228
+ fp_other = self.footprint(other)
229
+
230
+ remainder_roots = self._get_root_vars(remainder)
231
+ other_roots = self._get_root_vars(other)
232
+ remainder_symbolic_values = self._get_symbolic_values(remainder)
233
+
234
+ # Special case: List segments with same root
235
+ # ls(x,y) * ls(x,z) is only SAT when x=y=z (both empty)
236
+ # Let Z3 discover this constraint instead of blocking early
237
+ if isinstance(remainder, PredicateCall) and isinstance(other, PredicateCall):
238
+ if remainder.name == "ls" and other.name == "ls":
239
+ if remainder.args and other.args:
240
+ # Check if both have the same root variable
241
+ rem_root = remainder.args[0]
242
+ other_root = other.args[0]
243
+ if isinstance(rem_root, Var) and isinstance(other_root, Var):
244
+ if rem_root.name == other_root.name:
245
+ # Same root - allow Z3 to handle the constraints
246
+ return True
247
+
248
+ # Check 1: Locations must be disjoint
249
+ if not remainder_roots.isdisjoint(fp_other):
250
+ return False
251
+
252
+ # Check 2: Order-aware symbolic value check
253
+ if remainder_symbolic_values:
254
+ # Check if kept has symbolic footprint (predicates with _next)
255
+ has_symbolic_in_kept = any(v.endswith('_next') for v in fp_other)
256
+
257
+ if has_symbolic_in_kept:
258
+ # Check if remainder value matches kept predicate root
259
+ if not remainder_symbolic_values.isdisjoint(other_roots):
260
+ # Example: x |-> z * list(z) where value z IS the root
261
+ return False
262
+
263
+ # ORDER-AWARE HEURISTIC:
264
+ # If remainder occurs AFTER kept predicate → safe to drop (affine weakening)
265
+ # If remainder occurs BEFORE kept predicate → unsafe (conservative)
266
+ if remainder_pos < other_pos:
267
+ # Remainder before kept → may be referenced
268
+ # Example: x |-> y * list(z) where position(x|->y) < position(list(z))
269
+ return False
270
+ else:
271
+ # Remainder after kept → safe
272
+ # Example: list(x) * y |-> z where position(list(x)) < position(y|->z)
273
+ return True
274
+
275
+ # No symbolic values or no predicates → safe
276
+ return True
277
+
278
+ def can_drop_safely(self, remainder: Formula, other: Formula) -> bool:
279
+ """
280
+ Check if remainder can be safely dropped (backwards compatibility).
281
+
282
+ This version flattens both formulas and checks all combinations.
283
+ For simple cases (single formulas), assigns same position.
284
+ """
285
+ # Flatten both to get positions
286
+ remainder_flat = self._flatten_sepconj(remainder)
287
+ other_flat = self._flatten_sepconj(other)
288
+
289
+ # Check all combinations
290
+ for r_formula, r_pos in remainder_flat:
291
+ for o_formula, o_pos in other_flat:
292
+ if not self.can_drop_safely_order_aware(r_formula, r_pos, o_formula, o_pos):
293
+ return False
294
+
295
+ return True
296
+
297
+ def entails_emp_footprint_aware(self, antecedent: Formula, consequent: Formula) -> bool:
298
+ """
299
+ Check if antecedent can be weakened to emp when consequent is emp.
300
+
301
+ This implements affine weakening with footprint safety:
302
+ - If antecedent is emp, trivially true
303
+ - If antecedent has concrete allocations disjoint from consequent, can drop
304
+ - Otherwise, cannot safely weaken to emp
305
+
306
+ Args:
307
+ antecedent: The formula we're checking (remainder after frame rule)
308
+ consequent: Should be emp or contain spatial parts
309
+
310
+ Returns:
311
+ True if antecedent can safely entail emp in this context
312
+ """
313
+ if isinstance(antecedent, Emp):
314
+ return True
315
+
316
+ fp_ante = self.footprint(antecedent)
317
+
318
+ # If antecedent has no footprint, it's effectively emp
319
+ if not fp_ante:
320
+ return True
321
+
322
+ # If consequent is emp, check if antecedent footprint is disjoint from anything
323
+ if isinstance(consequent, Emp):
324
+ # In affine SL, we can drop cells that don't interfere
325
+ # But we can't drop cells if they're the *only* thing
326
+ # (i.e., x |-> y ⊬ emp when there's nothing else)
327
+ # This is the soundness check
328
+ return False # Cannot weaken non-empty to emp without context
329
+
330
+ # If consequent has spatial parts, check overlap
331
+ fp_cons = self.footprint(consequent)
332
+
333
+ # Can drop antecedent if its footprint doesn't overlap with consequent
334
+ return fp_ante.isdisjoint(fp_cons)