aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/phase4_diff.py
blob: 296e6d7b6dd8ea055e068f2c09ab88ed3f2e9217 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
"""Phase 4: adjacent-version diffs over normalised decompilation.

Adjacent (not endpoint) comparison, so a change introduced and then reverted
inside the range cannot hide. Classes per pair:
  CHANGED   present in both, normalised text differs  -> the interesting set
  ADDED     only in the newer build
  REMOVED   only in the older build
  IDENTICAL present in both, text equal
Functions absent from a build (UNMATCHED in Phase 2) are reported as coverage,
never silently dropped.
"""
import json, glob, os, re, sys, collections

TOKRE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|\S")

def tokens(t): return TOKRE.findall(t)

def semantically_equal(x, y):
    """Compare token streams treating SYM (an UNMATCHED callee, i.e. *unknown*)
    as a wildcard. Porting coverage differs between builds, so the same callee can
    be SYM in one and named in the other; that is a coverage artifact, not a
    semantic change. Any other token mismatch -- including every numeric literal --
    is a real difference."""
    a, b = tokens(x), tokens(y)
    if len(a) != len(b): return False
    for p, q in zip(a, b):
        if p == q: continue
        if p == 'SYM' or q == 'SYM': continue   # unknown vs known
        return False
    return True

def vkey(t):
    m=re.findall(r'(\d+)\.(\d+)\.(\d+)\.(\d+)', t)
    return [int(x) for x in m[0]] if m else [0,0,0,0]

def load(fam):
    out={}
    for p in sorted(glob.glob('phase3/%s_*.json'%fam)):
        out[os.path.basename(p)[:-5]]=json.load(open(p))
    return dict(sorted(out.items(), key=lambda kv: vkey(kv[0])))

LIT = re.compile(r"\b(?:0[xX][0-9A-Fa-f]+|\d+)\b")
def skeleton(t):
    """Structure + literals, with ALL identifiers erased. Two functions whose
    skeletons match differ only in which names were assigned -- which, for
    duplicate stubs, is an arbitrary porting choice, not a semantic change.
    Literals are retained, so a changed bound/mask/size still shows up."""
    return TOKRE.sub(lambda m: (m.group(0) if (LIT.fullmatch(m.group(0)) or not m.group(0)[0].isalpha()
                                               and m.group(0)[0] != '_') else 'ID'), t)

def diff_pair(a, b):
    ka,kb=set(a),set(b)
    raw=[k for k in ka&kb if not semantically_equal(a[k], b[k])]
    # split: SEMANTIC (structure or literals differ) vs NAME-ONLY (identifier churn)
    semantic=sorted(k for k in raw if skeleton(a[k]) != skeleton(b[k]))
    nameonly=sorted(set(raw)-set(semantic))
    return dict(changed=semantic, name_only=nameonly, added=sorted(kb-ka),
                removed=sorted(ka-kb), identical=len(ka&kb)-len(raw), comparable=len(ka&kb))

if __name__=='__main__':
    fams=sys.argv[1:] or ['sos','sys_drv','hdcp','asd']
    allres={}
    for fam in fams:
        builds=load(fam)
        names=list(builds)
        print("\n### %s : %d builds"%(fam.upper(), len(names)))
        for i in range(len(names)-1):
            o,n=names[i],names[i+1]
            d=diff_pair(builds[o], builds[n])
            allres["%s -> %s"%(o,n)]=d
            print("  %-34s -> %-34s cmp=%-5d SEMANTIC=%-4d name-only=%-4d ADDED=%-4d REM=%d"%(
                o.replace(fam+'_',''), n.replace(fam+'_',''), d['comparable'],
                len(d['changed']), len(d['name_only']), len(d['added']), len(d['removed'])))
    json.dump(allres, open('phase4_diffs.json','w'), indent=1)
    print("\nwrote phase4_diffs.json")