From 280bbe3faf8bb956ea70e052af4ff61a6a2ccb1a Mon Sep 17 00:00:00 2001
From: RetroGameDeveloper
<40120498+RetroGameDeveloper@users.noreply.github.com>
Date: Wed, 28 Oct 2026 22:15:50 +0000
Subject: [PATCH] Add cancellable deep permuter searches for a selected
function
---
README.md | 8 ++
ROADMAP.md | 6 +
src/decompnet/app/deep_permuter.py | 150 ++++++++++++++++++++++
src/decompnet/app/draft_repairs.py | 4 +-
src/decompnet/app/routes/projects.py | 9 ++
src/decompnet/app/static/index.html | 12 ++
src/decompnet/app/static/js/function.js | 42 ++++++-
tests/gui_progress.js | 35 +++++-
tests/test_app_server.py | 24 ++++
tests/test_deep_permuter.py | 159 ++++++++++++++++++++++++
10 files changed, 441 insertions(+), 8 deletions(-)
create mode 100644 src/decompnet/app/deep_permuter.py
create mode 100644 tests/test_deep_permuter.py
diff --git a/README.md b/README.md
index bbcf9821..32978b6f 100644
--- a/README.md
+++ b/README.md
@@ -623,6 +623,14 @@ explicit coverage notice identifies their exclusion from function progress.
### Permuter budgets
+For a longer search of one function, open its drawer and expand **Deep permuter · this function**.
+Choose a time limit and maximum candidate compiles, then select **Run deep permuter**. **Stop** ends the job
+after its current compiler/generator finishes. Better candidates and exact matches are saved immediately.
+This bypasses the background queue’s instruction and turn limits for the selected function only. It visits the
+current local candidate list, then uses random rewrites; it cannot exhaust every possible C program. No AI tokens
+are used. Cached misses are skipped on later deep runs when their build inputs still agree.
+
+
In **Settings → Permuter limits**, raise the maximum instructions per function to
include larger near misses. Defaults are 400 instructions, 60 seconds per turn and
four turns per function. All three limits are global, saved across restarts and
diff --git a/ROADMAP.md b/ROADMAP.md
index 64ffdbce..7a525450 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -875,3 +875,9 @@ Measured MIPS stack-only diffs can also propose two small missing-padding hypoth
epilogue and every changed saved-register slot agree on the same positive frame delta. The optional iterative
phase uses its existing budget; agent local trials require measured evidence for their current source. Other
processors still use padding removal/shrinking until they have a corresponding frame reader.
+
+A function drawer can launch a bounded deep permuter job for one saved, scored candidate in ROM or imported
+projects. It bypasses the background queue's size and turn filters, visits the generated local repair list,
+then uses ordinary random rewrites. Time/compile limits, progress, cancellation, normal judges and immediate
+saving remain mandatory. Scoped misses survive another deep run; failed compiles stay retryable and cached
+improvements must be rejudged. This does not claim to exhaust all possible C programs or compiler options.
diff --git a/src/decompnet/app/deep_permuter.py b/src/decompnet/app/deep_permuter.py
new file mode 100644
index 00000000..4a6af690
--- /dev/null
+++ b/src/decompnet/app/deep_permuter.py
@@ -0,0 +1,150 @@
+"""User-requested, bounded searches of one saved candidate, using the ordinary permuter judges.
+
+The finite local candidate list can be visited completely. Random C rewriting has no finite exhaustive
+search space, so time/variant limits and cancellation always remain explicit.
+"""
+import hashlib
+import json
+import time
+
+from ..network import NetworkError
+from ..project import compiler_for
+from . import agent_tokens, draft_repairs, permuter_local, stack_padding_repairs
+from .permute import Permuter, State
+
+
+class SearchLimit(Exception):
+ pass
+
+
+def validate(net, slug, name, minutes, max_variants):
+ if isinstance(minutes, bool) or not isinstance(minutes, int) or not 1 <= minutes <= 120:
+ raise NetworkError('Choose a time limit from 1 to 120 minutes.')
+ if isinstance(max_variants, bool) or not isinstance(max_variants, int) or not 1 <= max_variants <= 1000000:
+ raise NetworkError('Choose a variant limit from 1 to 1,000,000.')
+ proj = net.project_row(slug)
+ fn = net._function(name, slug)
+ if slug in net.links:
+ raise NetworkError('Run deep permuter on the room host’s project.')
+ if fn['status'] != 'open' or not fn['best_source'] or fn['best_score'] is None:
+ raise NetworkError('Choose an unmatched function with saved, scored C first.')
+ if net.store.one('SELECT 1 FROM claims WHERE function_id=? AND expires_at>?', fn['id'], time.time()):
+ raise NetworkError('This function is working; stop its agent or wait for its claim to finish.')
+ return proj, fn
+
+
+class Search(Permuter):
+ def __init__(self, net, minutes, max_variants, log):
+ super().__init__(net)
+ self.deadline = time.monotonic()+minutes*60
+ self.seconds = minutes*60
+ self.limit = max_variants
+ self.log = log
+ self.tested = self.skipped = self.visited = 0
+ self.best = None
+ self.phase = 'local repairs'
+ self.measuring_baseline = False
+
+ def check(self):
+ self.log.check_cancel()
+ if time.monotonic() >= self.deadline:
+ raise SearchLimit('Time limit reached')
+ if self.tested >= self.limit or self.visited >= self.limit*20:
+ raise SearchLimit('Variant limit reached')
+
+ def report(self):
+ self.log.progress({'phase': self.phase, 'tested': self.tested, 'skipped': self.skipped,
+ 'best_score': self.best, 'variant_limit': self.limit,
+ 'seconds_left': max(0, round(self.deadline-time.monotonic()))})
+
+ def _baseline(self, *args):
+ # A cached scalar score cannot supply the instruction rows needed by targeted repairs.
+ # The parent still caches a measured baseline within this search instance.
+ self.measuring_baseline = True
+ try:
+ return super()._baseline(*args)
+ finally:
+ self.measuring_baseline = False
+
+ def _score(self, agent, proj, fn, source, best, fork):
+ self.check()
+ self.visited += 1
+ # Scope includes the actual compiler/header/target and imported build dependencies. A previous
+ # miss is reusable only in that scope. Better cached results are rejudged before being kept.
+ scope = agent_tokens.target_scope(self.net, {**dict(fn), 'best_source': source}, '')
+ key = 'deep-v1:'+hashlib.sha256((scope+source).encode()).hexdigest()
+ cached = self.net.store.one('SELECT feedback FROM reuse_trials WHERE function_id=? AND signature=?', fn['id'], key)
+ score = json.loads(cached['feedback']) if cached and cached['feedback'] else None
+ if not self.measuring_baseline and isinstance(score, int) and score >= best:
+ self.skipped += 1
+ else:
+ self.tested += 1
+ score = super()._score(agent, proj, fn, source, best, fork)
+ if score is not None and score > 0:
+ with self.net.store.tx() as db:
+ db.execute('INSERT OR REPLACE INTO reuse_trials(function_id,signature,outcome,reason,feedback) VALUES(?,?,?,?,?)',
+ (fn['id'], key, 'miss', 'deep function search', json.dumps(score)))
+ if score is not None:
+ self.best = score if self.best is None else min(self.best, score)
+ self.report()
+ return score
+
+
+def run(net, slug, name, minutes, max_variants, log):
+ """Visit the supported local repairs, then spend the remaining budget on random permutations."""
+ proj, fn = validate(net, slug, name, minutes, max_variants)
+ search = Search(net, minutes, max_variants, log)
+ state = State()
+ agent, _ = net.local_agent('permuter')
+ reason = 'Current candidate families finished'
+ log('Testing supported local repairs, then bounded random rewrites. This cannot exhaust every possible C program.')
+ try:
+ if not search._hold(agent, fn, count_turn=False):
+ raise NetworkError('This function was claimed meanwhile; try again after it is released.')
+ try:
+ fork = net.fork_slug(slug)
+ flags = compiler_for(net.cfg(proj), fn['vram']).get('flags', []) if not fork else []
+ # Fresh baseline evidence enables measured stack repairs and diagnoses stale saved scores.
+ baseline, _ = search._baseline(agent, proj, fn, state, fork)
+ if baseline is None:
+ raise NetworkError('The saved C does not compile in the current context; fix it before deep search.')
+ if baseline == 0:
+ reason = 'Matched'
+ else:
+ best = baseline
+ source = fn['best_source']
+ rows = getattr(net, '_last_diffs', {}).get((agent.id, fn['id']), ([], ''))[0]
+ choices = draft_repairs.variants(source, fn['name'], proj['arch'], fn['asm'] or '', flags, limit=None)
+ choices += stack_padding_repairs.grow(source, fn['name'], proj['arch'], rows)
+ choices += [s for _,s in permuter_local.variants(source, fn['name'])]
+ for candidate in dict.fromkeys(choices):
+ score = search._score(agent, proj, fn, candidate, best, fork)
+ if score is not None and score < best:
+ best = score
+ if score == 0:
+ reason = 'Matched'
+ break
+ finally:
+ search._let_go(agent, fn)
+ if reason != 'Matched':
+ search.check()
+ search.phase = 'random rewrites'
+ search.seconds = max(1, search.deadline-time.monotonic())
+ current = net._function(name, slug)
+ if current['status'] == 'matched':
+ reason = 'Matched'
+ else:
+ # _permute ignores the normal size/turn queue filters; the user selected this function.
+ search._permute(agent, proj, current, state)
+ if state.tried == 0:
+ raise NetworkError('This function was claimed meanwhile; try again after it is released.')
+ if net._function(name, slug)['status'] == 'matched':
+ reason = 'Matched'
+ elif time.monotonic() >= search.deadline:
+ reason = 'Time limit reached'
+ except SearchLimit as error:
+ reason = str(error)
+ search.report()
+ log(f'{reason}: {search.tested} candidates tested, {search.skipped} cached misses skipped. Verified improvements are saved immediately.')
+ return {'function': name, 'reason': reason, 'tested': search.tested, 'skipped': search.skipped,
+ 'matched': net._function(name, slug)['status'] == 'matched', 'best_score': search.best}
diff --git a/src/decompnet/app/draft_repairs.py b/src/decompnet/app/draft_repairs.py
index b399914c..41e5694c 100644
--- a/src/decompnet/app/draft_repairs.py
+++ b/src/decompnet/app/draft_repairs.py
@@ -31,7 +31,7 @@
_MEMBER = rf'{_IDENT}(?:\s*(?:->|\.)\s*{_IDENT})*'
-def variants(source: str, name: str, arch: str, asm: str = '', flags=(), labels=None) -> list[str]:
+def variants(source: str, name: str, arch: str, asm: str = '', flags=(), labels=None, *, limit=LIMIT) -> list[str]:
span = find(source, name)
if span is None:
return []
@@ -155,4 +155,4 @@ def add(a, b, text):
for items in groups.values():
if items:
ordered.append(items.pop(0))
- return ordered[:LIMIT]
+ return ordered if limit is None else ordered[:limit]
diff --git a/src/decompnet/app/routes/projects.py b/src/decompnet/app/routes/projects.py
index 809f29bc..9493aed0 100644
--- a/src/decompnet/app/routes/projects.py
+++ b/src/decompnet/app/routes/projects.py
@@ -237,6 +237,14 @@ def work(log):
return {'job': jobs.start('source-preparation', work)}
return source_preparation.configure(net, proj, data)
+ def p_deep_permuter(slug, request, data):
+ from .. import deep_permuter
+ name = str(data.get('function') or '')
+ minutes, variants = data.get('minutes', 30), data.get('max_variants', 100000)
+ deep_permuter.validate(net, slug, name, minutes, variants)
+ return {'job': jobs.start('deep-permuter', lambda log: deep_permuter.run(net, slug, name, minutes, variants, log),
+ cancel_message='Deep permuter stopped. Verified improvements are saved.')}
+
def p_permuter(slug, request, data):
"""Run free local permutations without starting a paid helper or the full autopilot."""
net.project_row(slug)
@@ -324,6 +332,7 @@ def p_join_rom(slug, request, data):
"/api/try": (p_try, ["POST"]), "/api/release": (p_release, ["POST"]), "/api/reopen": (p_reopen, ["POST"]), "/api/setaside": (p_set_aside, ["POST"]),
"/api/recheck": (p_recheck, ["POST"]),
"/api/permuter": (p_permuter, ["POST"]),
+ "/api/permuter/function": (p_deep_permuter, ["POST"]),
"/api/source-preparation": (p_source_preparation, ["GET", "POST"]),
"/api/autopilot": (p_autopilot, ["POST"]), "/api/instructions": (p_instructions, ["POST"]), "/api/title": (p_title, ["POST"]),
"/api/cross-game": (lambda s, r, d: {"results": net.twins.cross_game_results(s),
diff --git a/src/decompnet/app/static/index.html b/src/decompnet/app/static/index.html
index ee06ca69..7cfa2b90 100644
--- a/src/decompnet/app/static/index.html
+++ b/src/decompnet/app/static/index.html
@@ -572,6 +572,18 @@
Comparison details
+
+ Deep permuter · this function
+
Tries the supported local repairs, then searches random rewrites of this function’s saved C. No AI tokens. This is a bounded search, not every possible C program. Exact matches and better candidates are saved immediately.