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
9 changes: 9 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,12 @@ These are not semantic proofs: best scores, saved best C and the exact judge rem
be rejudged in their current context. Custom generated dependencies outside the observed build/include files
still need richer build-system dependency records.

Shared deterministic repairs can stage a scalar store's byte address, route repeated pointer-to-integer casts
through an inline helper on supported 32-bit processors, remove/shrink unreferenced local padding, and narrow
an ordinary local when its existing uses already contain narrow casts. ROM and imported projects use the same
bounded draft/local-family pipelines and ordinary exact judges. These remain source hypotheses, not semantic
proofs or automatic compiler-setting changes; volatile, escaping and ambiguous cases are left alone.
Measured MIPS stack-only diffs can also propose two small missing-padding hypotheses when the prologue,
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.
16 changes: 16 additions & 0 deletions src/decompnet/app/draft_repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
expression_repairs,
lifetime_repairs,
load_pair_repairs,
local_width_repairs,
mmio_repairs,
near_repairs,
pattern_repairs,
pointer_cast_repairs,
register_repairs,
stack_padding_repairs,
stack_repairs,
storage_scope_repairs,
store_address_repairs,
)

LIMIT = 20
Expand Down Expand Up @@ -61,6 +65,18 @@ def variants(source: str, name: str, arch: str, asm: str = '', flags=(), labels=
for family,text in lifetime_repairs.variants(source,name):
proposals.append(text)
labels[text]=family
for text in store_address_repairs.variants(source, name, arch):
proposals.append(text)
labels[text] = 'store_address_lifetime'
for text in stack_padding_repairs.variants(source, name):
proposals.append(text)
labels[text] = 'stack_padding'
for text in local_width_repairs.variants(source, name):
proposals.append(text)
labels[text] = 'local_width'
for text in pointer_cast_repairs.variants(source, name, arch):
proposals.append(text)
labels[text] = 'pointer_cast_inline'
# Fresh m2c drafts already get this alternative in autopilot. Saved/transferred drafts need the
# same chance, with their macro typedef block kept out of the refinement's type substitutions.
if arch == 'mips' and ('M2C_FIELD(' in source or 'M2C_UNK' in source or re.search(r'\barg[0-3]\b',source)):
Expand Down
5 changes: 4 additions & 1 deletion src/decompnet/app/iterative_repairs.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ def output_key(report):


def targeted_variants(source, report, fn, arch, flags):
from . import draft_repairs, repair_ranking
from . import draft_repairs, repair_ranking, stack_padding_repairs
labels = {}
proposals = draft_repairs.variants(source, fn['name'], arch, fn['asm'] or '', flags, labels)
for text in stack_padding_repairs.grow(source, fn['name'], arch, report.get('rows') or []):
proposals.append(text)
labels[text] = 'stack_padding'
families = repair_ranking.order(list(dict.fromkeys(labels[p] for p in proposals)), report.get('rows') or [])
return sorted(proposals, key=lambda p: families.index(labels[p]))

Expand Down
17 changes: 16 additions & 1 deletion src/decompnet/app/local_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
expression_repairs,
lifetime_repairs,
load_pair_repairs,
local_width_repairs,
mmio_repairs,
pattern_repairs,
pointer_cast_repairs,
register_repairs,
repair_ranking,
stack_padding_repairs,
store_address_repairs,
)

FAMILIES = ('signedness','declaration_order','typed_temporary','branch_orientation','narrowing','source_lines',
'address_temporary','constant_local','comparison_load','stack_array','loop_induction',
'parameter_width','value_lifetime','sum_shape','call_result_inline','comparison_reuse','integer_address_order','mmio_address','call_arity','struct_byte_offset','switch_call','paired_load_order','array_index_width','split_scalar_lifetime','pointer_before_store')
'parameter_width','value_lifetime','sum_shape','call_result_inline','comparison_reuse','integer_address_order','mmio_address','call_arity','struct_byte_offset','switch_call','paired_load_order','array_index_width','split_scalar_lifetime','pointer_before_store','store_address_lifetime','stack_padding','local_width','pointer_cast_inline')


def run(net, agent, slug, function, revision, families=None, max_trials=6):
Expand Down Expand Up @@ -60,6 +64,17 @@ def run(net, agent, slug, function, revision, families=None, max_trials=6):
for family,candidate in lifetime_repairs.variants(source,fn['name']):
if family in pools:
pools[family].append(candidate)
if 'pointer_cast_inline' in pools:
pools['pointer_cast_inline'] = pointer_cast_repairs.variants(source,fn['name'],proj['arch'])
if 'local_width' in pools:
pools['local_width'] = local_width_repairs.variants(source,fn['name'])
if 'stack_padding' in pools:
pools['stack_padding'] = stack_padding_repairs.variants(source,fn['name'])
measured = getattr(net,'_agent_outcomes',{}).get((agent.id,fn['id']))
if measured and measured.get('source') == source:
pools['stack_padding'] += stack_padding_repairs.grow(source,fn['name'],proj['arch'],measured.get('rows') or [])
if 'store_address_lifetime' in pools:
pools['store_address_lifetime'] = store_address_repairs.variants(source,fn['name'],proj['arch'])
if 'paired_load_order' in pools:
pools['paired_load_order']=load_pair_repairs.variants(source,fn['name'])
if 'mmio_address' in pools:
Expand Down
40 changes: 40 additions & 0 deletions src/decompnet/app/local_width_repairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Try a narrower local when the candidate already casts that value to a narrow type.

A byte store's cast can be too late to reproduce the original allocator. These hypotheses change only
an ordinary scalar declaration; the exact judge, not the cast alone, decides whether to keep them.
"""
import re

from ..forks.csource import _blank, find

IDENT = r'[A-Za-z_]\w*'


def variants(source, name):
span = find(source, name)
if not span:
return []
clean = _blank(source)
opening = clean.find('{', *span)
if opening < 0 or '#' in clean[opening:span[1]] or re.search(r'\bvolatile\b', clean):
return []
body = clean[opening:span[1]]
result = []
declaration = re.compile(r'(?:^|(?<=[;{}]))\s*(?P<type>s32|u32|int|unsigned\s+int)\s+(?P<name>'+IDENT+r')\s*;')
for hit in declaration.finditer(body):
var = hit['name']
if re.search(r'&\s*'+re.escape(var)+r'\b', body):
continue
cast = re.search(r'\(\s*(s8|u8|char|signed\s+char|unsigned\s+char|s16|u16|short|unsigned\s+short)\s*\)\s*'+re.escape(var)+r'\b', body[hit.end():])
if not cast:
continue
# A name reused in an inner scope is ambiguous; don't retype a different declaration.
if len(re.findall(r'\b(?:s32|u32|int|unsigned\s+int|char|short|[su](?:8|16))\s+'+re.escape(var)+r'\s*[;=,]', body)) != 1:
continue
choices = ('char', 'signed char', 'unsigned char') if cast[1] in {'s8','u8','char','signed char','unsigned char'} else ('short','unsigned short')
for typ in choices:
a, b = opening+hit.start('type'), opening+hit.end('type')
result.append(source[:a]+typ+source[b:])
if len(result) >= 6:
break
return result[:6]
54 changes: 54 additions & 0 deletions src/decompnet/app/pointer_cast_repairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Inline a repeated pointer-to-integer cast through a small helper for old register allocators.

The source must declare the operand as a pointer. This preserves evaluations and keeps the compiler's
inlining choice provisional: only the ordinary exact judge can accept the resulting object.
"""
import re

from ..forks.csource import _blank, find

IDENT = r'[A-Za-z_]\w*'


def variants(source, name, arch):
if arch not in {'mips', 'ppc', 'x86', 'arm', 'thumb'}:
return []
span = find(source, name)
if not span:
return []
clean = _blank(source)
opening = clean.find('{', *span)
if opening < 0 or '#' in clean[opening:span[1]] or re.search(r'\bvolatile\b', clean):
return []
helper = '__repair_pointer_bits'
if re.search(r'\b'+helper+r'\b', clean):
return []
pointers = re.findall(r'\b(?:void|char|[su](?:8|16|32)|unsigned\s+(?:char|short|int))\s*\*\s*('+IDENT+r')\s*[;,)]', clean)
body = clean[opening:span[1]]
cast = re.compile(r'\(\s*(?P<type>s32|u32|int|unsigned\s+int)\s*\)\s*(?P<expr>'+IDENT+r'(?:\s*->\s*'+IDENT+r')*)(?!\w|\s*\()')
groups = {}
for hit in cast.finditer(body):
expr = re.sub(r'\s+', '', hit['expr'])
pointer = expr.split('->')[-1]
if pointers.count(pointer) != 1:
continue
# Don't take a prefix out of indexed/longer expressions or post-increment a pointer.
if re.match(r'\s*(?:\[|\.|->|\+\+|--)', body[hit.end():]):
continue
groups.setdefault((hit['type'], expr), []).append(hit)
result = []
for (typ, expr), hits in groups.items():
if len(hits) < 2:
continue
text = source
for hit in reversed(hits):
a, b = opening+hit.start(), opening+hit.end()
operand = source[opening+hit.start('expr'):opening+hit.end('expr')]
text = text[:a]+helper+'('+operand+')'+text[b:]
# Built-in return types make the helper independent of typedef placement in the candidate.
returned = 'unsigned int' if typ in {'u32', 'unsigned int'} else 'int'
prefix = 'static inline '+returned+' '+helper+'(void *value)\n{\n return ('+returned+')value;\n}\n\n'
result.append(prefix+text)
if len(result) >= 2:
break
return result
8 changes: 4 additions & 4 deletions src/decompnet/app/repair_ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ def order(families, rows):
if focus == 'call':
preferred = ('call_arity', 'call_result_inline', 'parameter_width', 'parameter_storage')
elif markers and markers <= {'r', 's'}:
preferred = ('unused_scalar_local', 'local_storage', 'parameter_storage', 'local_declaration_order',
'stack_array', 'parameter_width', 'declaration_order') if 's' in markers else (
'comparison_reuse', 'integer_address_order', 'typed_temporary', 'value_lifetime',
'split_scalar_lifetime', 'array_index_width', 'declaration_order')
preferred = ('stack_padding', 'unused_scalar_local', 'local_storage', 'parameter_storage',
'local_declaration_order', 'stack_array', 'parameter_width', 'declaration_order') if 's' in markers else (
'store_address_lifetime', 'pointer_cast_inline', 'comparison_reuse', 'integer_address_order',
'typed_temporary', 'value_lifetime', 'split_scalar_lifetime', 'array_index_width', 'local_width', 'declaration_order')
elif markers & {'<', '>', '|', 'o'}:
preferred = ('paired_load_order', 'pointer_before_store', 'branch_orientation', 'loop_induction', 'call_result_inline', 'parameter_width')
elif 'i' in markers:
Expand Down
59 changes: 59 additions & 0 deletions src/decompnet/app/stack_padding_repairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Remove or shrink unused local padding; exact comparison decides the original frame size."""
import re

from .. import arch as arches
from ..forks.csource import _blank, find

IDENT = r'[A-Za-z_]\w*'
TYPES = r'(?:[su](?:8|16|32)|char|short|int|unsigned\s+(?:char|short|int))'


def variants(source, name):
span = find(source, name)
if not span:
return []
clean = _blank(source)
opening = clean.find('{', *span)
if opening < 0 or '#' in clean[opening:span[1]]:
return []
body = clean[opening:span[1]]
declaration = re.compile(r'(?:^|(?<=[;{}]))\s*(?P<decl>'+TYPES+r'\s+(?P<name>(?:pad|padding)\w*)\s*'
r'\[\s*(?P<count>0x[\da-fA-F]+|\d+)\s*\]\s*;)')
result = []
for hit in declaration.finditer(body):
# Only ordinary declarations in the function's outer scope, never aggregate fields,
# initialized arrays, qualifiers or storage whose address/sizeof is referenced.
prefix = body[:hit.start('decl')]
if prefix.count('{')-prefix.count('}') != 1:
continue
if len(re.findall(r'\b'+re.escape(hit['name'])+r'\b', body)) != 1:
continue
count = int(hit['count'], 16 if hit['count'].startswith('0x') else 10)
if not 1 <= count <= 64:
continue
a, b = opening+hit.start('decl'), opening+hit.end('decl')
result.append(source[:a]+source[b:])
if count > 1:
a, b = opening+hit.start('count'), opening+hit.end('count')
result.append(source[:a]+str(count//2)+source[b:])
if len(result) >= 4:
break
return result


def grow(source, name, arch, rows):
"""Try missing local storage only when every differing instruction agrees on one frame delta."""
reader = arches.get(arch).stack_frame_growth
delta = reader(rows) if reader else None
if delta is None:
return []
span = find(source, name)
if not span:
return []
clean = _blank(source)
opening = clean.find('{', *span)
fresh = '__repair_stack_padding'
if opening < 0 or '#' in clean[opening:span[1]] or re.search(r'\b(?:volatile|'+fresh+r')\b', clean):
return []
return [source[:opening+1]+'\nchar '+fresh+'['+str(count)+'];'+source[opening+1:]
for count in (delta, delta//2)]
79 changes: 79 additions & 0 deletions src/decompnet/app/store_address_repairs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Stage a scalar store's byte address without moving its loads across other statements.

Older compilers can choose a different operand order for an address kept in a pointer temporary.
These bounded source hypotheses still need the ordinary linked/object judge to accept them.
"""
import re

from ..forks.csource import _blank, find
from .register_repairs import unwrap

IDENT = r'[A-Za-z_]\w*'
SCALAR = r'(?:[su](?:8|16|32)|char|short|int|unsigned\s+(?:char|short|int))'
NUMBER = r'(?:0x[0-9a-fA-F]+|[0-9]+)'


def split_sum(text):
"""Find a binary addition outside casts and nested address expressions."""
depth = 0
for pos, token in enumerate(text):
depth += (token in '([') - (token in ')]')
if token == '+' and depth == 0:
return text[:pos].strip(), text[pos+1:].strip()
return None


def variants(source, name, arch):
if arch not in {'mips', 'ppc', 'x86', 'arm', 'thumb'}:
return []
span = find(source, name)
if not span:
return []
clean = _blank(source)
opening = clean.find('{', *span)
if opening < 0 or re.search(r'\bvolatile\b', clean) or '#' in clean[opening:span[1]]:
return []
body = clean[opening+1:span[1]-1]
result = []
fresh = '__repair_store_address'
if re.search(r'\b'+fresh+r'\b', clean):
return []
# The RHS is a simple value: do not reorder an address load around a call or another store.
store = re.compile(r'(?:^|(?<=[;{}]))\s*(?P<lhs>\*[^;{}]+?)\s*=(?!=)\s*(?P<rhs>'+IDENT+'|'+NUMBER+r')\s*;')
for hit in store.finditer(body):
lhs = unwrap(hit['lhs'][1:])
cast = re.match(r'\(\s*('+SCALAR+r')\s*\*\s*\)', lhs)
if not cast:
continue
address = unwrap(lhs[cast.end():])
outer = split_sum(address)
if not outer or not re.fullmatch(NUMBER, outer[1]):
continue
bytecast = re.match(r'\(\s*(?:char|u8|s8|unsigned\s+char)\s*\*\s*\)', unwrap(outer[0]))
if not bytecast:
continue
term = unwrap(outer[0])
summed = split_sum(unwrap(term[bytecast.end():]))
if not summed:
continue
left, right = summed
# One base identifier and one nontrivial offset. No calls, increments, assignments or comma
# operators, and no unknown character tokens. Every original load is evaluated exactly once.
if re.fullmatch(IDENT, right):
base, index = right, left
elif re.fullmatch(IDENT, left):
base, index = left, right
else:
continue
if not re.search(r'[\[\]*]', index) or re.search(r'\+\+|--|[=,?:;{}]|\b'+IDENT+r'\s*\(', index):
continue
replacement = ('{ char *'+fresh+' = (char *)'+base+'; '+fresh+' += '+index+'; '
'*('+cast[1]+' *)('+fresh+' + '+outer[1]+') = '+hit['rhs']+'; }')
start = opening+1+hit.start('lhs')
end = opening+1+hit.end()
candidate = source[:start]+replacement+source[end:]
if candidate != source and candidate not in result:
result.append(candidate)
if len(result) >= 4:
break
return result
6 changes: 4 additions & 2 deletions src/decompnet/arch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dataclasses import dataclass, field
from typing import Any

from . import arm, mips, mips_fields, mips_tools, ppc, x86, x86_fields
from . import arm, mips, mips_fields, mips_stack, mips_tools, ppc, x86, x86_fields

# What decompdecomp's readers can do with a processor's code (each is a module of its own that reads MIPS, and some
# x86):
Expand Down Expand Up @@ -46,13 +46,15 @@ class Arch:
method_draft: Callable[..., str | None] | None = None
# variants of it capstone can't decode (Console.cpu_variant) -> their own reader: (code, big_endian) ->
# [(offset, mnemonic, operands)], for scoring's diffs (MIPS: the PSP's Allegrex, read by rabbitizer)
# Uniform saved-register/frame shifts in scored instruction pairs, for bounded padding hypotheses.
stack_frame_growth: Callable[..., int | None] | None = None
variants: dict[str, Callable[[bytes, bool], list[tuple[int, str, str]]]] = field(default_factory=dict)


ARCHES = [
Arch("mips", "MIPS", mips.CAPSTONE, mips.RELOC_MASKS, mips.asm_reason, True,
frozenset({COMPILER_MAP, CLASSES, BOUNDARIES, STRUCTS}), mips_fields.read_facts,
build_tool_paths=mips_tools.build_paths, variants={mips.ALLEGREX: mips.allegrex_lines}),
build_tool_paths=mips_tools.build_paths, stack_frame_growth=mips_stack.frame_growth, variants={mips.ALLEGREX: mips.allegrex_lines}),
Arch("ppc", "PowerPC", ppc.CAPSTONE, ppc.RELOC_MASKS, ppc.asm_reason),
Arch("x86", "x86", x86.CAPSTONE, x86.RELOC_MASKS, features=frozenset({CLASSES, STRUCTS}),
field_accesses=x86_fields.read_facts, float_operands=x86.float_operands, method_draft=x86.method_draft),
Expand Down
Loading
Loading