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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,10 @@ new candidates permanently.
### 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
Choose a time limit and maximum candidate compiles, then select **Run deep permuter**.
The scorecard shows the freshly measured starting score and percentage, the current best, and how many
verified score reductions this run found. Local repairs and random rewrites share that improvement count.
Expand **Technical output** for compiler diagnostics; the progress bar stays visible when output is closed. **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
Expand Down
36 changes: 29 additions & 7 deletions src/decompnet/app/deep_permuter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import time

from .. import scoring
from ..network import NetworkError
from ..project import compiler_for
from . import agent_tokens, draft_repairs, permuter_local, stack_padding_repairs
Expand Down Expand Up @@ -34,14 +35,18 @@ def validate(net, slug, name, minutes, max_variants):


class Search(Permuter):
def __init__(self, net, minutes, max_variants, log):
def __init__(self, net, minutes, max_variants, log, fn=None):
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 = self.rejected = 0
self.best = None
self.starting_score = fn['best_score'] if fn is not None else None
self.best = self.starting_score
self.max_score = max(1, fn['n_insns'] or 0) * scoring.PENALTY_INSERTION if fn is not None else None
self.improvements = 0
self.baseline_measured = False
self.phase = 'local repairs'
self.measuring_baseline = False

Expand All @@ -52,9 +57,18 @@ def check(self):
if self.tested >= self.limit or self.visited >= self.limit*20:
raise SearchLimit('Variant limit reached')

def metrics(self):
"""Use the ordinary score scale; a near miss must never round up to a displayed 100%."""
def percent(score):
if score is None or self.max_score is None:
return None
return 100.0 if score == 0 else min(99.9, int(max(0, 100*(1-score/self.max_score))*10)/10)
return {'starting_score': self.starting_score, 'starting_percent': percent(self.starting_score),
'best_score': self.best, 'best_percent': percent(self.best), 'improvements': self.improvements}

def report(self):
self.log.progress({'phase': self.phase, 'tested': self.tested, 'skipped': self.skipped,
'best_score': self.best, 'variant_limit': self.limit, 'rejected': self.rejected,
**self.metrics(), 'variant_limit': self.limit, 'rejected': self.rejected,
'seconds_left': max(0, round(self.deadline-time.monotonic()))})

def _baseline(self, *args):
Expand Down Expand Up @@ -82,7 +96,8 @@ def _score(self, agent, proj, fn, source, best, fork):
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:
reused = not self.measuring_baseline and isinstance(score, int) and score >= best
if reused:
self.skipped += 1
else:
self.tested += 1
Expand All @@ -92,15 +107,22 @@ def _score(self, agent, proj, fn, source, best, fork):
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)
if self.measuring_baseline and not self.baseline_measured:
self.starting_score = self.best = score
self.baseline_measured = True
elif not reused and not self.measuring_baseline and (self.best is None or score < self.best):
if self.best is not None:
self.improvements += 1
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)
search = Search(net, minutes, max_variants, log, fn)
search.report()
state = State()
agent, _ = net.local_agent('permuter')
reason = 'Current candidate families finished'
Expand Down Expand Up @@ -158,4 +180,4 @@ def run(net, slug, name, minutes, max_variants, log):
search.report()
log(f'{reason}: {search.tested} candidates tested, {search.skipped} cached misses skipped, {search.rejected} invalid calls skipped. Verified improvements are saved immediately.')
return {'function': name, 'reason': reason, 'tested': search.tested, 'skipped': search.skipped,
'rejected': search.rejected, 'matched': net._function(name, slug)['status'] == 'matched', 'best_score': search.best}
'rejected': search.rejected, 'matched': net._function(name, slug)['status'] == 'matched', **search.metrics()}
33 changes: 33 additions & 0 deletions src/decompnet/app/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1118,3 +1118,36 @@ details.more[open] > summary::before { content: "− "; }
.agent-results-dialog header p { margin: 0; font: 12px var(--mono); overflow-wrap: anywhere; }
.agent-result-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; padding: 12px 0; border-bottom: 1px solid var(--line); }
.agent-result-row button { text-align: left; overflow-wrap: anywhere; font-family: var(--mono); }

/* Deep search keeps its scores visible while optional technical output grows separately. */
.deep-search { padding: 16px; border: 1px solid var(--line); border-radius: var(--r); background: var(--raise); }
.deep-search > summary { font-weight: 600; }
.deep-free { display: inline-block; margin-left: 8px; padding: 3px 7px; border: 1px solid var(--line); border-radius: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
.deep-controls { display: grid; grid-template-columns: minmax(100px, 1fr) minmax(140px, 1fr) auto; gap: 12px; align-items: end; margin: 18px 0; }
.deep-controls .field { margin: 0; min-width: 0; }
.deep-controls input { width: 100%; box-sizing: border-box; }
.deep-actions { display: flex; gap: 8px; flex-wrap: wrap; padding-bottom: 1px; }
.deep-dashboard { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
.deep-dashboard[hidden] { display: none; }
.deep-scores { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; }
.deep-scores > div { padding: 16px; min-width: 0; }
.deep-scores > div + div { border-left: 1px solid var(--line); }
.deep-scores dt, .deep-scores span { color: var(--muted); font-size: 11px; }
.deep-scores dd { margin: 8px 0 5px; font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; }
.deep-scores span { display: block; overflow-wrap: anywhere; }
.deep-best { background: color-mix(in oklch, var(--hl-soft) 16%, transparent); }
.deep-dashboard[data-matched="true"] .deep-best dd { color: var(--match); }
.deep-dashboard[data-improved="true"] { border-color: var(--hl); }
.deep-counters { display: flex; flex-wrap: wrap; gap: 8px 20px; padding: 10px 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.deep-counters b { color: var(--ink); font-weight: 600; }
.deep-counters > :last-child { margin-left: auto; }
.deep-output { margin-top: 14px; font-size: 12px; }
.deep-output summary { cursor: pointer; color: var(--muted); }
.deep-output .joblog { max-height: 180px; margin-top: 10px; }
.deep-note { margin-bottom: 0; font-size: 11px; }
@media (max-width: 650px) {
.deep-controls { grid-template-columns: 1fr 1fr; }
.deep-actions { grid-column: 1 / -1; }
.deep-scores > div { padding: 12px 8px; }
.deep-scores dd { font-size: 22px; }
}
29 changes: 20 additions & 9 deletions src/decompnet/app/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -572,17 +572,27 @@ <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>
<details class="more deep-search" id="fnDeepPanel" hidden>
<summary>Deep permuter · this function <span class="deep-free">No AI tokens</span></summary>
<p class="small muted">Try local repairs, then random rewrites. Verified improvements save immediately.</p>
<div class="deep-controls">
<label class="field"><span>Time limit · minutes</span><input id="fnDeepMinutes" type="number" min="1" max="120" value="30"></label>
<label class="field"><span>Compile limit</span><input id="fnDeepVariants" type="number" min="1" max="1000000" value="100000"></label>
<div class="deep-actions"><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></div>
</div>
<div class="deep-dashboard" id="fnDeepMetrics" hidden>
<dl class="deep-scores">
<div><dt>Starting match</dt><dd id="fnDeepStartPercent">—</dd><span id="fnDeepStartScore">Score —</span></div>
<div class="deep-best"><dt>Current best</dt><dd id="fnDeepBestPercent">—</dd><span id="fnDeepBestScore">Score —</span></div>
<div><dt>Improvements</dt><dd id="fnDeepImproved">0</dd><span>Verified score reductions</span></div>
</dl>
<div class="deep-counters"><span><b id="fnDeepTested">0</b> tested</span><span><b id="fnDeepSkipped">0</b> cached skips</span><span id="fnDeepTime">—</span></div>
</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>
<div id="fnDeepWork"></div>
<details class="deep-output" id="fnDeepOutput"><summary>Technical output</summary><pre class="joblog" id="fnDeepLog" hidden></pre></details>
<p class="small muted deep-note">Search is bounded by these limits; it cannot try every possible C program.</p>
</details>
<div class="fn-tabs">
<div class="tabs" role="tablist" aria-label="Function view">
Expand Down Expand Up @@ -646,6 +656,7 @@ <h2 id="fnName" class="mono"></h2>
<script src="/assets/js/agents.js"></script>
<script src="/assets/js/transcripts.js"></script>
<script src="/assets/js/repair-feedback.js"></script>
<script src="/assets/js/deep-progress.js"></script>
<script src="/assets/js/function.js"></script>
<script src="/assets/js/compiler-progress.js"></script>
<script src="/assets/js/steering.js"></script>
Expand Down
22 changes: 22 additions & 0 deletions src/decompnet/app/static/js/deep-progress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use strict";
/* A compact live scorecard, independent of the detailed compiler log. */
function deepProgressView() {
const ids = ["fnDeepMetrics", "fnDeepStartPercent", "fnDeepStartScore", "fnDeepBestPercent", "fnDeepBestScore",
"fnDeepImproved", "fnDeepTested", "fnDeepSkipped", "fnDeepTime"];
const el = Object.fromEntries(ids.map((id) => [id, $(id)]));
const number = (value) => value == null ? "—" : value.toLocaleString();
const percent = (value) => value == null ? "—" : `${value === 100 ? "100" : (Math.floor(value * 10 + 1e-8) / 10).toFixed(1)}%`;
return (p = {}) => {
el.fnDeepMetrics.hidden = false;
el.fnDeepStartPercent.textContent = percent(p.starting_percent);
el.fnDeepStartScore.textContent = `Score ${number(p.starting_score)}`;
el.fnDeepBestPercent.textContent = percent(p.best_percent);
el.fnDeepBestScore.textContent = `Score ${number(p.best_score)}`;
el.fnDeepImproved.textContent = number(p.improvements ?? 0);
el.fnDeepTested.textContent = number(p.tested ?? 0);
el.fnDeepSkipped.textContent = number(p.skipped ?? 0);
el.fnDeepTime.textContent = p.seconds_left == null ? "" : `${Math.floor(p.seconds_left / 60)}m ${p.seconds_left % 60}s left`;
el.fnDeepMetrics.dataset.matched = String(p.best_percent === 100);
el.fnDeepMetrics.dataset.improved = String((p.improvements ?? 0) > 0);
};
}
15 changes: 13 additions & 2 deletions src/decompnet/app/static/js/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ async function refreshFn(first) {
? `${d.unit} · ${d.insns} instructions${d.match_percent != null ? ` · ${d.match_percent >= 100 ? "100" : (Math.floor(d.match_percent * 10) / 10).toFixed(1)}% now` : ""}`
: `${hex8(d.vram)} · ROM 0x${d.rom_offset.toString(16).toUpperCase()} · ${d.insns} instructions`;
if ($("fnName").textContent !== d.name) {
if (!deepPermuterBusy) {
$("fnDeepMetrics").hidden = true;
$("fnDeepStatus").textContent = "";
}
$("fnCompilerRanked").replaceChildren();
$("fnApplyCompiler").hidden = true;
$("fnCompilerSummary").hidden = true;
Expand Down Expand Up @@ -532,26 +536,33 @@ $("fnDeepRun").onclick = async () => {
const log = $("fnDeepLog"), status = $("fnDeepStatus"), stop = $("fnDeepStop");
const controls = [$("fnDeepRun"), $("fnDeepMinutes"), $("fnDeepVariants")];
const minutes = Number($("fnDeepMinutes").value), maxVariants = Number($("fnDeepVariants").value);
const renderProgress = deepProgressView();
renderProgress();
log._followTail = false;
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.");
const bar = log.previousElementSibling;
$("fnDeepWork").append(log._workStatus);
$("fnDeepWork").append(bar);
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`;
if (p) { renderProgress(p); status.textContent = `${name} · ${p.phase}`; }
});
renderProgress(result);
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);
else { status.textContent = error.message; $("fnDeepOutput").open = true; showJobError(log, error.message); }
} finally {
deepPermuterBusy = false; deepPermuterJob = null; stop.hidden = true; done();
}
Expand Down
19 changes: 19 additions & 0 deletions tests/gui_progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function setup(file, marker, end, logId, buttonId, ranges = "") {
},
};
vm.createContext(ctx);
vm.runInContext(read("deep-progress"), ctx);
const base = read("base");
vm.runInContext(base.slice(base.indexOf("const JOB_POLL_MS"), base.indexOf("/* The map's still")), ctx);
// The page error box is independent of the bar; keep its text to check that cleanup preserves it.
Expand Down Expand Up @@ -230,6 +231,24 @@ async function main() {
copyTest.ctx.showJobError(copyTest.log, "");
assert.equal(toolbar.hidden, true);
assert.equal(copyTest.log.textContent, "");
// Deep-search scores remain visible independently of the collapsed technical log.
{
const test = setup(...cases[cases.length - 1]);
const running = test.ctx.run();
assert.ok(test.get("fnDeepWork").children.includes(test.log.bar));
assert.equal(test.log._followTail, false);
test.start.resolve({job:"deep"}); await tick();
const metrics = {starting_score:14639,starting_percent:76.3,best_score:0,best_percent:100,
improvements:4,tested:268,skipped:1,reason:"Matched",seconds_left:165};
test.poll.resolve({state:"done",log:[],progress:metrics,result:metrics}); await running;
assert.equal(test.get("fnDeepStartPercent").textContent,"76.3%");
assert.equal(test.get("fnDeepBestPercent").textContent,"100%");
assert.equal(test.get("fnDeepBestScore").textContent,"Score 0");
assert.equal(test.get("fnDeepImproved").textContent,"4");
assert.equal(test.get("fnDeepTime").textContent,"2m 45s left");
assert.equal(test.get("fnDeepMetrics").dataset.matched,"true");
assert.equal(test.log.bar.removed,true);
}
// A range-only compiler setting remains immediate and has no progress bar.
const range = setup(...cases[9], "0x1000-0x2000");
const running = range.ctx.run();
Expand Down
Loading
Loading