Constant expression hoisting for iteration loops
Problem
When an expression like does_exist primesum(n,2) == trisum(666) is evaluated, the RHS trisum(666) contains no free variables — it's a constant. But the current evaluation engine recomputes it on every iteration of n, turning an O(1) computation into O(N) redundant calls.
For cheap functions this is negligible. For expensive ones like trisum(666) (which computes a 98-digit number by summing 36 rows of base-666 digit arrangements), the overhead is significant — especially when N could be in the billions.
Proposed solution
Constant folding / expression hoisting: Before entering the iteration loop, walk the AST and identify subexpressions that contain no free variables (no iteration variables like n, m, etc.). Evaluate these once and replace them with their computed values.
Detection strategy
def find_free_vars(node) -> set:
"""Return set of variable names referenced in this AST node."""
if isinstance(node, Variable): return {node.name}
if isinstance(node, FunctionCall): return union(find_free_vars(arg) for arg in node.args)
if isinstance(node, BinaryOp): return find_free_vars(node.left) | find_free_vars(node.right)
...
def hoist_constants(expr, iter_vars):
"""Pre-evaluate subexpressions with no free iteration variables."""
free = find_free_vars(expr)
if not (free & iter_vars): # No overlap with iteration variables
return Literal(evaluate(expr)) # Compute once, replace with constant
# Otherwise recurse into children
...
Examples
| Expression |
Free vars |
Hoisted? |
primesum(n,2) |
{n} |
No — depends on iteration var |
trisum(666) |
{} |
Yes — computed once |
primesum(n,2) == trisum(666) |
LHS: {n}, RHS: {} |
RHS hoisted |
primesum(n, m) |
{n, m} |
No — both are iteration vars |
tri(36) + 1 |
{} |
Yes — entire subexpression hoisted |
primesum(n, 2) == 666 |
LHS: {n}, RHS: {} |
RHS already a literal (no change) |
Implementation location
The hoisting pass should run in find_matches() (in prime-square-sum.py) after parsing and before the iteration loop begins. The AST is already available from the grammar parser.
Impact
- Core use case:
does_exist primesum(n,2) == trisum(666) — the project's central question. Without hoisting, this recomputes a 98-digit number billions of times.
- General benefit: Any expression mixing iteration variables with constant function calls benefits automatically.
- No API change: Pure internal optimization, transparent to users.
Related
Constant expression hoisting for iteration loops
Problem
When an expression like
does_exist primesum(n,2) == trisum(666)is evaluated, the RHStrisum(666)contains no free variables — it's a constant. But the current evaluation engine recomputes it on every iteration ofn, turning an O(1) computation into O(N) redundant calls.For cheap functions this is negligible. For expensive ones like
trisum(666)(which computes a 98-digit number by summing 36 rows of base-666 digit arrangements), the overhead is significant — especially when N could be in the billions.Proposed solution
Constant folding / expression hoisting: Before entering the iteration loop, walk the AST and identify subexpressions that contain no free variables (no iteration variables like
n,m, etc.). Evaluate these once and replace them with their computed values.Detection strategy
Examples
primesum(n,2)trisum(666)primesum(n,2) == trisum(666)primesum(n, m)tri(36) + 1primesum(n, 2) == 666Implementation location
The hoisting pass should run in
find_matches()(inprime-square-sum.py) after parsing and before the iteration loop begins. The AST is already available from the grammar parser.Impact
does_exist primesum(n,2) == trisum(666)— the project's central question. Without hoisting, this recomputes a 98-digit number billions of times.Related