Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@
| 1044 edges -> 2647 facts | 0.0142s | 0.0131s | -7.7% | 34/41 | -0.0011s vs 0.0018s -- **within** |

So: a small, consistently-signed lean toward #133 that does **not** separate from noise at this sample size. Note that the summary statistic is itself unstable -- a second run of the smaller shape gave -5.2% and 30/41 -- which is the point. An earlier note in this file claimed 0.037s against 0.048s -- roughly 23% -- and that number is **retracted**: it was min-of-7 across separate processes, which picks up the tail of the distribution rather than a difference between the arms. What does hold at every measurement: the two derive identical closures, and the micro-benchmark of `_unify` alone is a real 1.7-2x, diluted at `materialise` level by how often unification actually binds a new variable -- a property of the rule shape, not a constant. A bare speed-up number in this file will be reused on a workload it was never true for.

## 2026-07-28 - Pre-compute properties outside recursive generator blocks
**Learning:** In `src/tacet/core/symbolic.py`, Datalog join evaluations within `RuleEngine._join` experienced significant overhead by delegating to `_unify` inside the inner recursive `extend` loop. Extracting static pattern property checks out of the recursion and manually inlining unification, along with safely querying dictionaries with `.get()` to avoid empty allocations, improved extreme hot paths performance by roughly 60%.
**Action:** When optimizing extreme hot paths in Python (e.g., deeply recursive search loops), precalculate static variables in the outer loop block and inline the core evaluation logic directly into recursive paths to eliminate constant function call overhead and premature allocations. Use provided helper functions like `_is_var` when doing so to ensure behavioral consistency.
60 changes: 50 additions & 10 deletions src/tacet/core/symbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,24 +292,64 @@ def _join(
order the level-by-level version did, so the derivation a fact is
recorded with — and therefore its proof tree — is unchanged.
"""
# ⚡ Bolt Optimization: Pre-compute static checks and inline unification logic
body_info = [(s, r, o, _is_var(s), _is_var(r), _is_var(o)) for s, r, o in body]

def extend(depth: int, binding: dict[str, str]) -> Iterator[dict[str, str]]:
if depth == len(body):
yield binding
return
s, r, o = body[depth]
s_val = binding.get(s) if _is_var(s) else s
o_val = binding.get(o) if _is_var(o) else o
s, r, o, s_is_var, r_is_var, o_is_var = body_info[depth]
s_val = binding.get(s) if s_is_var else s
o_val = binding.get(o) if o_is_var else o

if s_val is not None:
candidates: list[Triple] = idx_subj.get((r, s_val), [])
candidates = idx_subj.get((r, s_val))
elif o_val is not None:
candidates = idx_obj.get((r, o_val), [])
candidates = idx_obj.get((r, o_val))
else:
candidates = idx_all.get(r, [])
for fact in candidates:
merged = _unify((s, r, o), fact, binding)
if merged is not None:
yield from extend(depth + 1, merged)
candidates = idx_all.get(r)

if candidates is None:
return

for t0, t1, t2 in candidates:
if not s_is_var:
if s != t0:
continue
elif s in binding and binding[s] != t0:
continue

if not r_is_var:
if r != t1:
continue
elif r == s:
if t1 != t0:
continue
elif r in binding and binding[r] != t1:
continue

if not o_is_var:
if o != t2:
continue
elif o == s:
if t2 != t0:
continue
elif o == r:
if t2 != t1:
continue
elif o in binding and binding[o] != t2:
continue

merged = binding.copy()
if s_is_var:
merged[s] = t0
if r_is_var:
merged[r] = t1
if o_is_var:
merged[o] = t2

yield from extend(depth + 1, merged)

return extend(0, {})

Expand Down
Loading