Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
150 changes: 150 additions & 0 deletions src/decompnet/app/deep_permuter.py
Original file line number Diff line number Diff line change
@@ -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}
4 changes: 2 additions & 2 deletions src/decompnet/app/draft_repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down Expand Up @@ -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]
9 changes: 9 additions & 0 deletions src/decompnet/app/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
12 changes: 12 additions & 0 deletions src/decompnet/app/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,18 @@ <h2 id="fnName" class="mono"></h2>
<button class="primary small-btn" id="fnApplyCompiler" type="button" hidden>Apply these settings to this function</button>
<details class="more"><summary>Comparison details</summary><pre class="joblog" id="fnCompilerLog" hidden></pre></details>
</div>
<details class="more" id="fnDeepPanel" hidden>
<summary>Deep permuter · this function</summary>
<p class="small muted">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.</p>
<div class="fields">
<label class="field"><span>Time limit (minutes)</span><input id="fnDeepMinutes" type="number" min="1" max="120" value="30"></label>
<label class="field"><span>Maximum candidate compiles</span><input id="fnDeepVariants" type="number" min="1" max="1000000" value="100000"></label>
</div>
<button class="primary small-btn" id="fnDeepRun" type="button">Run deep permuter</button>
<button class="ghost small-btn" id="fnDeepStop" type="button" hidden>Stop</button>
<p class="small muted" id="fnDeepStatus" role="status"></p>
<pre class="joblog" id="fnDeepLog" hidden></pre>
</details>
<div class="fn-tabs">
<div class="tabs" role="tablist" aria-label="Function view">
<button role="tab" class="tab on" data-ftab="diff" type="button">Diff</button>
Expand Down
42 changes: 41 additions & 1 deletion src/decompnet/app/static/js/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ document.querySelectorAll("#diffWhich [data-which]").forEach((b) => {

let fnBusy = false;
let compilerComparisonBusy = false;
let deepPermuterBusy = false, deepPermuterJob = null;
async function refreshFn(first) {
if (fnBusy || nameChangeBusy || !S.fn) return;
fnBusy = true;
Expand Down Expand Up @@ -124,6 +125,7 @@ async function refreshFn(first) {
? `Name inferred from ${d.naming.source_project} · exact code match`
: d.renamed ? `Renamed from ${d.naming.original_name}` : d.named ? "Named function" : "Generated name";
fnBadge(d);
$("fnDeepPanel").hidden = !deepPermuterBusy && (d.status !== "open" || d.best == null);
$("fnCompilerTest").hidden = S.ov.kind === "fork" || d.status !== "open" || !S.ov.compilers?.length;
if (!compilerComparisonBusy && S.ov.kind !== "fork") {
const history = d.compiler_test;
Expand Down Expand Up @@ -523,4 +525,42 @@ $("fnTestCompilers").onclick = async () => {
renderFunctionCompilerComparison(result.comparison, slug, name);
} catch (e) { summary.textContent = e.message; showJobError(log, e.message); }
finally { compilerComparisonBusy = false; finish(); await refreshFn(false); }
};
};

$("fnDeepRun").onclick = async () => {
const slug = S.slug, name = S.fn;
const log = $("fnDeepLog"), status = $("fnDeepStatus"), stop = $("fnDeepStop");
const controls = [$("fnDeepRun"), $("fnDeepMinutes"), $("fnDeepVariants")];
const minutes = Number($("fnDeepMinutes").value), maxVariants = Number($("fnDeepVariants").value);
deepPermuterBusy = true;
log.hidden = false; stop.hidden = false; stop.disabled = true;
showJobError(log, "");
status.textContent = `Starting deep search for ${name}…`;
const done = beginWork(log, "Searching this function locally", controls, "Deep search finished.");
try {
const { job } = await api("/api/permuter/function", { method: "POST", body: { slug, function: name, minutes, max_variants: maxVariants } });
deepPermuterJob = job; stop.disabled = false;
const result = await followJob(job, log, (_, state) => {
const p = state.progress;
if (p) status.textContent = `${name} · ${p.phase} · ${p.tested.toLocaleString()} tested · ${p.skipped.toLocaleString()} cached misses skipped · best score ${p.best_score ?? "—"} · ${p.seconds_left}s left`;
});
status.textContent = `${name} · ${result.reason} · ${result.tested.toLocaleString()} tested. Improvements saved.`;
if (S.slug === slug && S.fn === name) { await refreshMap(true); await refreshFn(false); }
} catch (error) {
if (error.job?.state === "cancelled") {
status.textContent = `${name} · Stopped. Verified improvements are saved.`;
log._workStatus.dataset.state = "cancelled"; log._workStatus.textContent = "Stopped";
}
else showJobError(log, error.message);
} finally {
deepPermuterBusy = false; deepPermuterJob = null; stop.hidden = true; done();
}
};
$("fnDeepStop").onclick = async () => {
if (!deepPermuterJob) return;
const button = $("fnDeepStop"); button.disabled = true;
try {
await api("/api/job/cancel", { method: "POST", body: { id: deepPermuterJob } });
$("fnDeepStatus").textContent = "Stopping after the current compiler or generator finishes. Improvements are saved.";
} catch (error) { showJobError($("fnDeepLog"), error.message); button.disabled = false; }
};
Loading
Loading