4.10 literal-source campaign: manifest DP upgrade, Route A closed, toolchain facts
- manifest410.py: monotone-DP TU labeling (data-cluster affinity + CLASSMAP seeds + recon @addr soft votes) replaces naive fill; BT region = 917 fn / ~230 KB, extents match every independently-known TU boundary. - source-completeness: ROUTE A CLOSED (Nick holds no further assets); toolchain = BC++ 4.52 PROVEN by CW32.LIB byte-match, archived at TeslaRel410/BORLAND; OPT.MAK = the shipped binary's exact recipe; extender corrected to Borland PowerPack DPMI32 (was "Phar Lap TNT"); "all BT headers survive" corrected (17/36 bt-side; mech.hpp etc. reconstruction-only); engine gaps enumerated (vdata.hpp first, back-dated from BT412). - phases/phase-03: rounds 1-3 of the source410 campaign - 6/10 surviving originals compile clean under BC4.52; console wire IDs recovered from the binary ctors (Killed=9 Damaged=10 ScoreUpdate=13 DWH=15 [T1], TeamScore guessed 12 [T4] - answers the TeslaSuite console-port spec's open item); round-3 finding: MECH.HPP is the capstone grown with the mech TU reconstructions, BTREG.CPP green = the header-family milestone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+249
-43
@@ -75,11 +75,21 @@ CENSUS_SET = set(CENSUS)
|
||||
# ------------------------------------------------------- section dump -------
|
||||
def load_string_table():
|
||||
"""Parse section_dump.txt (objdump -s style: ' VA GGGGGGGG x4 ascii')
|
||||
into a VA->byte map; strings are read lazily via read_cstr()."""
|
||||
into (VA->byte map, [(section_name, lo, hi)]); strings are read lazily
|
||||
via read_cstr()."""
|
||||
mem = {}
|
||||
sections = [] # (name, lo, hi)
|
||||
cur_name, cur_lo, cur_hi = None, None, None
|
||||
pat = re.compile(r"^ ([0-9a-f]{6,8}) ((?:[0-9a-f]{2,8} ){1,4})", re.I)
|
||||
sec = re.compile(r"^Contents of section (\S+):")
|
||||
with open(SECTION_DUMP, "r", errors="replace") as f:
|
||||
for line in f:
|
||||
sm = sec.match(line)
|
||||
if sm:
|
||||
if cur_name is not None:
|
||||
sections.append((cur_name, cur_lo, cur_hi))
|
||||
cur_name, cur_lo, cur_hi = sm.group(1), None, None
|
||||
continue
|
||||
m = pat.match(line)
|
||||
if not m:
|
||||
continue
|
||||
@@ -91,7 +101,12 @@ def load_string_table():
|
||||
for i in range(0, len(group), 2):
|
||||
mem[va + off] = int(group[i:i + 2], 16)
|
||||
off += 1
|
||||
return mem
|
||||
if cur_lo is None:
|
||||
cur_lo = va
|
||||
cur_hi = va + off
|
||||
if cur_name is not None:
|
||||
sections.append((cur_name, cur_lo, cur_hi))
|
||||
return mem, sections
|
||||
|
||||
def read_cstr(mem, va, maxlen=260):
|
||||
out = []
|
||||
@@ -113,11 +128,13 @@ def normalize_path(p):
|
||||
|
||||
# ------------------------------------------------------------ decomp --------
|
||||
FUNC_HDR = re.compile(r"^/\* @([0-9a-f]{8}) file=(\S+) name=(\S+) \*/")
|
||||
SYM_REF = re.compile(r"s_\w+?_(00[0-9a-f]{6})\b")
|
||||
SYM_LINE = re.compile(r"s_\w+?_(00[0-9a-f]{6})\s*,\s*(0x[0-9a-f]+|\d+)\s*[),]")
|
||||
# every symbolic reference into the image (code or data)
|
||||
ANY_REF = re.compile(r"\b(?:s_\w+?|DAT|PTR_FUN|PTR_DAT|PTR_LAB|UNK)_(00[0-9a-f]{6})\b")
|
||||
|
||||
def parse_decomp(mem):
|
||||
"""Return sorted list of dicts: addr, name, tag, part, paths{norm: [lines]}."""
|
||||
"""Return sorted list of dicts:
|
||||
addr, name, tag, part, paths{norm:[lines]}, refs[set of VAs]."""
|
||||
funcs = []
|
||||
for fn in sorted(os.listdir(DECOMP_DIR)):
|
||||
if not fn.endswith(".c"):
|
||||
@@ -134,60 +151,211 @@ def parse_decomp(mem):
|
||||
"name": m.group(3),
|
||||
"part": part,
|
||||
"paths": defaultdict(list), # norm path -> [line numbers]
|
||||
"refs": set(), # referenced VAs
|
||||
}
|
||||
funcs.append(cur)
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
# line-number pairs first (subset of SYM_REF hits)
|
||||
consumed = set()
|
||||
for sm in SYM_LINE.finditer(line):
|
||||
va = int(sm.group(1), 16)
|
||||
s = read_cstr(mem, va)
|
||||
if PATH_RE.match(s):
|
||||
num = int(sm.group(2), 0)
|
||||
cur["paths"][normalize_path(s)].append(num)
|
||||
consumed.add(va)
|
||||
for sm in SYM_REF.finditer(line):
|
||||
cur["paths"][normalize_path(s)].append(int(sm.group(2), 0))
|
||||
for sm in ANY_REF.finditer(line):
|
||||
va = int(sm.group(1), 16)
|
||||
if va in consumed:
|
||||
continue
|
||||
cur["refs"].add(va)
|
||||
s = read_cstr(mem, va)
|
||||
if PATH_RE.match(s):
|
||||
cur["paths"][normalize_path(s)] # touch, no line
|
||||
cur["paths"][normalize_path(s)] # touch (no line info)
|
||||
funcs.sort(key=lambda d: d["addr"])
|
||||
return funcs
|
||||
|
||||
# ------------------------------------------------------- attribution --------
|
||||
def attribute(funcs):
|
||||
"""Set f['file'] (best attribution) + f['how'] in
|
||||
{assert, tag, fill, boundary, none}."""
|
||||
#
|
||||
# Anchors are sparse (release build: only hard Fail() sites carry file
|
||||
# strings), so attribution leans on a second linker invariant: each OBJ's
|
||||
# DATA (vtables, literals, statics) is laid out contiguously per section in
|
||||
# the SAME link order as its code. Every data VA referenced by an anchored
|
||||
# function becomes a known (VA -> TU) point; an unanchored function's data
|
||||
# refs then VOTE: a ref whose nearest known neighbors below AND above in the
|
||||
# same section agree contributes one vote for that TU. Assignments feed
|
||||
# back as new known points/anchors and the pass iterates to a fixed point,
|
||||
# then plain link-order fill closes the gaps between agreeing code anchors.
|
||||
#
|
||||
def _data_sections(sections):
|
||||
out = [(n, lo, hi) for (n, lo, hi) in sections
|
||||
if n.upper() != "CODE" and lo is not None]
|
||||
# .bss is not in the dump (no contents) but its statics are laid out in
|
||||
# the same per-OBJ link order -- treat the VA range above the last dumped
|
||||
# data section as a virtual section so bss refs can vote too.
|
||||
data_hi = max(hi for (n, lo, hi) in out if n.upper() == "DATA")
|
||||
out.append((".bss~", data_hi, 0x540000))
|
||||
return out
|
||||
|
||||
# Verified TU anchor addresses from game/reconstructed/CLASSMAP.md and the
|
||||
# context files ([T1] each: ctors / named functions whose owning TU is
|
||||
# established). These seed the data-cluster voting where Fail() strings are
|
||||
# absent, and let the link-order sanity check run.
|
||||
SEED_ANCHORS = {
|
||||
0x49B54C: "bt/btmssn.cpp", # BTMission ctor
|
||||
0x49BCA4: "bt/messmgr.cpp", # SubsystemMessageManager ctor
|
||||
0x49DE14: "bt/dmgtable.cpp", # DamageZonePercentTable SelectZone
|
||||
0x49EA48: "bt/dmgtable.cpp", # DamageLookupTable ctor
|
||||
0x49F624: "bt/mech.cpp", # mech<->player bind
|
||||
0x4A2D48: "bt/mech.cpp", # Mech::Make
|
||||
0x4ADDA0: "bt/heat.cpp", # heat subsystem ctor (file= tagged)
|
||||
0x4AFBE0: "bt/mechmppr.cpp", # CycleControlMode
|
||||
0x4B0EFC: "bt/powersub.cpp", # PoweredSubsystem::HandleMessage
|
||||
0x4B3778: "bt/gyro.cpp", # Gyroscope ctor
|
||||
0x4B6918: "bt/torso.cpp", # Torso::Recenter
|
||||
0x4B8D18: "bt/myomers.cpp", # MyomersSimulation
|
||||
0x4B9728: "bt/mechweap.cpp", # MechWeapon::SendDamageMessage
|
||||
0x4BBFCC: "bt/projweap.cpp", # CheckForJam
|
||||
0x4BEF78: "bt/missile.cpp", # Missile MoveAndCollide
|
||||
0x4C0BC8: "bt/btplayer.cpp", # BTPlayer ctor
|
||||
0x4C1C24: "bt_l4/btl4rdr.cpp", # MapDisplay ctor
|
||||
0x4C2F94: "bt_l4/btl4gaug.cpp", # gauge TU start (CLASSMAP extent)
|
||||
0x4C6798: "bt_l4/btl4gau2.cpp", # gau2 TU start (CLASSMAP extent)
|
||||
0x4CC40C: "bt_l4/btl4vid.cpp", # BTReticleRenderable ctor
|
||||
0x4D1AE4: "bt_l4/btl4mppr.cpp", # L4MechControlsMapper::SetControlMode
|
||||
0x4D2C30: "bt_l4/btl4mssn.cpp", # BTL4Mission::SetPlayerData
|
||||
0x4D34C4: "bt_l4/btl4app.cpp", # BTL4Application ctor
|
||||
0x4D3BE4: "bt_l4/btl4pb.cpp", # BTL4PlaybackApplication ctor
|
||||
}
|
||||
|
||||
def _section_of(va, dsecs):
|
||||
for i, (_, lo, hi) in enumerate(dsecs):
|
||||
if lo <= va < hi:
|
||||
return i
|
||||
return None
|
||||
|
||||
def attribute(funcs, sections, recon_affinity=None):
|
||||
"""Set f['file'] + f['how'] in {assert, seed, tag, dp, rtl, none}.
|
||||
recon_affinity: {addr: tu_key} soft votes from name-matched
|
||||
game/reconstructed files' @address annotations."""
|
||||
dsecs = _data_sections(sections)
|
||||
recon_affinity = recon_affinity or {}
|
||||
|
||||
# pass 1: direct evidence
|
||||
for f in funcs:
|
||||
best = None
|
||||
if f["paths"]:
|
||||
# the file MOST referenced in-body (asserts overwhelmingly name
|
||||
# the owning TU); ties broken by presence of line numbers
|
||||
best = max(
|
||||
f["paths"].items(),
|
||||
key=lambda kv: (len(kv[1]) > 0, len(kv[1]), kv[0]),
|
||||
)[0]
|
||||
f["file"], f["how"] = best, "assert"
|
||||
elif f["addr"] in SEED_ANCHORS:
|
||||
f["file"], f["how"] = SEED_ANCHORS[f["addr"]], "seed"
|
||||
elif f["tag"] != "?":
|
||||
f["file"], f["how"] = f["tag"].lower(), "tag"
|
||||
else:
|
||||
f["file"], f["how"] = None, "none"
|
||||
|
||||
# pass 2: link-order fill between agreeing anchors
|
||||
anchors = [(i, f["file"]) for i, f in enumerate(funcs) if f["file"]]
|
||||
for (i0, f0), (i1, f1) in zip(anchors, anchors[1:]):
|
||||
for j in range(i0 + 1, i1):
|
||||
if funcs[j]["file"] is None:
|
||||
if f0 == f1:
|
||||
funcs[j]["file"], funcs[j]["how"] = f0, "fill"
|
||||
else:
|
||||
funcs[j]["file"], funcs[j]["how"] = None, "boundary"
|
||||
funcs[j]["between"] = (f0, f1)
|
||||
# ---- pass 2: monotone DP labeling of the BT region -------------------
|
||||
#
|
||||
# bt.lib + btl4.lib are the last game libs before the Borland RTL, so
|
||||
# inside [first BT anchor .. RTL sentinel) the TU label sequence along
|
||||
# the address axis must be NON-DECREASING in makefile link order. A DP
|
||||
# picks, among all monotone labelings that respect the anchors, the one
|
||||
# maximizing data-cluster affinity. The REGION TOTALS are exact under
|
||||
# the sentinels even where an individual TU boundary is soft.
|
||||
#
|
||||
import bisect
|
||||
|
||||
# anchor-known data points (assert/seed/tag only -- no cascade); VAs
|
||||
# referenced from >1 file are shared globals: non-discriminative.
|
||||
claims = defaultdict(set)
|
||||
for f in funcs:
|
||||
if f["file"] is not None:
|
||||
for va in f["refs"]:
|
||||
claims[va].add(f["file"])
|
||||
shared = {va for va, owners in claims.items() if len(owners) > 1}
|
||||
known = [[] for _ in dsecs]
|
||||
for f in funcs:
|
||||
if f["file"] is None:
|
||||
continue
|
||||
for va in f["refs"]:
|
||||
if va not in shared:
|
||||
si = _section_of(va, dsecs)
|
||||
if si is not None:
|
||||
known[si].append((va, f["file"]))
|
||||
for k in known:
|
||||
k.sort()
|
||||
|
||||
def affinity(f):
|
||||
votes = defaultdict(float)
|
||||
for va in f["refs"]:
|
||||
if va in shared:
|
||||
continue
|
||||
si = _section_of(va, dsecs)
|
||||
if si is None or not known[si]:
|
||||
continue
|
||||
k = known[si]
|
||||
i = bisect.bisect_left(k, (va, ""))
|
||||
if i < len(k) and k[i][0] == va:
|
||||
votes[k[i][1]] += 2.0 # unique exact VA match
|
||||
continue
|
||||
lo = k[i - 1][1] if i > 0 else None
|
||||
hi = k[i][1] if i < len(k) else None
|
||||
if lo is not None and lo == hi:
|
||||
votes[lo] += 1.0 # inside one TU's data cluster
|
||||
return votes
|
||||
|
||||
RTL_SENTINEL = 0x4D4B58 # first known Borland RTL fn
|
||||
# btl4.obj is linked right after the startup OBJ (see BTL4.MAK tlink32
|
||||
# response), i.e. at the BOTTOM of the image -- it is NOT part of the
|
||||
# bt.lib/btl4.lib region and is excluded from the ordered labeling.
|
||||
labels = [t for t in CENSUS if t != "bt_l4/btl4.cpp"] + ["<rtl>"]
|
||||
label_ix = {t: i for i, t in enumerate(labels)}
|
||||
lo_i = next(i for i, f in enumerate(funcs)
|
||||
if f["file"] in CENSUS_SET)
|
||||
hi_i = next(i for i, f in enumerate(funcs) if f["addr"] >= RTL_SENTINEL)
|
||||
region = funcs[lo_i:hi_i + 1]
|
||||
NEG = -1e18
|
||||
n, m = len(region), len(labels)
|
||||
score = [[0.0] * m for _ in range(n)]
|
||||
for r, f in enumerate(region):
|
||||
if f["file"] in label_ix: # anchored: pin it
|
||||
for c in range(m):
|
||||
score[r][c] = 0.0 if c == label_ix[f["file"]] else NEG
|
||||
elif f["addr"] >= RTL_SENTINEL:
|
||||
for c in range(m):
|
||||
score[r][c] = 0.0 if labels[c] == "<rtl>" else NEG
|
||||
else:
|
||||
votes = affinity(f)
|
||||
t = recon_affinity.get(f["addr"])
|
||||
if t is not None:
|
||||
votes[t] += 0.75 # port citation, soft
|
||||
for t, v in votes.items():
|
||||
if t in label_ix:
|
||||
score[r][label_ix[t]] = v
|
||||
# DP: best[r][c] = max over c' <= c of best[r-1][c'] + score[r][c]
|
||||
best = [row[:] for row in score]
|
||||
back = [[0] * m for _ in range(n)]
|
||||
for c in range(1, m):
|
||||
if best[0][c] < best[0][c - 1]:
|
||||
best[0][c] = best[0][c - 1]
|
||||
for r in range(1, n):
|
||||
run_best, run_ix = NEG, 0
|
||||
for c in range(m):
|
||||
if best[r - 1][c] > run_best:
|
||||
run_best, run_ix = best[r - 1][c], c
|
||||
back[r][c] = run_ix
|
||||
best[r][c] = (run_best + score[r][c]) if run_best > NEG / 2 \
|
||||
and score[r][c] > NEG / 2 else NEG
|
||||
# trace back from the best final cell
|
||||
c = max(range(m), key=lambda cc: best[n - 1][cc])
|
||||
for r in range(n - 1, -1, -1):
|
||||
f = region[r]
|
||||
if f["file"] is None:
|
||||
f["file"] = labels[c]
|
||||
f["how"] = "dp"
|
||||
c = back[r][c]
|
||||
|
||||
for f in funcs:
|
||||
if f["file"] == "<rtl>":
|
||||
f["file"], f["how"] = None, "rtl"
|
||||
return funcs
|
||||
|
||||
# ------------------------------------------------- reconstructed map --------
|
||||
@@ -228,12 +396,24 @@ def survivors():
|
||||
# ------------------------------------------------------------- main ---------
|
||||
def main():
|
||||
print("loading section dump ...")
|
||||
mem = load_string_table()
|
||||
print(" %d bytes mapped" % len(mem))
|
||||
mem, sections = load_string_table()
|
||||
print(" %d bytes mapped, sections: %s" % (
|
||||
len(mem), ", ".join("%s@%x-%x" % s for s in sections)))
|
||||
print("parsing decomp ...")
|
||||
funcs = parse_decomp(mem)
|
||||
print(" %d functions" % len(funcs))
|
||||
attribute(funcs)
|
||||
# soft affinity: a recon file named like a census TU votes for that TU
|
||||
# at every address it cites (cross-citations are outvoted by the DP's
|
||||
# monotone constraint)
|
||||
recon_pre = recon_addresses()
|
||||
recon_affinity = {}
|
||||
for fn, addrs in recon_pre.items():
|
||||
base = os.path.splitext(fn)[0].lower()
|
||||
for key in (tu_key("bt", base), tu_key("bt_l4", base)):
|
||||
if key in CENSUS_SET:
|
||||
for a in addrs:
|
||||
recon_affinity[a] = key
|
||||
attribute(funcs, sections, recon_affinity)
|
||||
|
||||
# sizes: delta to next function
|
||||
for f, g in zip(funcs, funcs[1:]):
|
||||
@@ -246,7 +426,7 @@ def main():
|
||||
|
||||
# per-TU aggregation (census TUs + whatever engine files showed up)
|
||||
tus = defaultdict(lambda: {
|
||||
"funcs": 0, "bytes": 0, "assert": 0, "fill": 0, "tag": 0,
|
||||
"funcs": 0, "bytes": 0, "assert": 0, "data": 0, "fill": 0, "tag": 0,
|
||||
"lo": None, "hi": None, "max_line": 0, "recon_hits": 0,
|
||||
})
|
||||
boundary = 0
|
||||
@@ -261,7 +441,8 @@ def main():
|
||||
t = tus[f["file"]]
|
||||
t["funcs"] += 1
|
||||
t["bytes"] += f["size"]
|
||||
t[f["how"] if f["how"] in ("assert", "fill", "tag") else "tag"] += 1
|
||||
bucket = {"assert": "assert", "dp": "fill", "data": "data"}.get(f["how"], "tag")
|
||||
t[bucket] += 1
|
||||
t["lo"] = f["addr"] if t["lo"] is None else min(t["lo"], f["addr"])
|
||||
t["hi"] = max(t["hi"] or 0, f["addr"] + f["size"])
|
||||
for lines in f["paths"].values():
|
||||
@@ -275,30 +456,51 @@ def main():
|
||||
t = tus.get(key)
|
||||
s = surv.get(key, "")
|
||||
if not t:
|
||||
return "| %s | %s | — | — | — | — | — | 0 |" % (key, s or "—")
|
||||
return "| %s | %s | %d | %.1f | %06x-%06x | %d | %d | %d |" % (
|
||||
return "| %s | %s | — | — | — | — | — | — | 0 |" % (key, s or "—")
|
||||
return "| %s | %s | %d | %.1f | %06x-%06x | %d | %d | %d | %d |" % (
|
||||
key, s or "—", t["funcs"], t["bytes"] / 1024.0,
|
||||
t["lo"], t["hi"], t["assert"], t["max_line"], t["recon_hits"],
|
||||
t["lo"], t["hi"], t["assert"], t["data"] + t["fill"],
|
||||
t["max_line"], t["recon_hits"],
|
||||
)
|
||||
|
||||
bt_keys = [tu_key("bt", n) for n in BT_LIB_TUS]
|
||||
btl4_keys = [tu_key("bt_l4", n) for n in BTL4_LIB_TUS + MAIN_TUS]
|
||||
engine_keys = sorted(k for k in tus if k not in CENSUS_SET)
|
||||
|
||||
# link-order sanity: census TU code ranges must ascend in makefile order
|
||||
order_violations = []
|
||||
prev = None
|
||||
for k in bt_keys + btl4_keys:
|
||||
t = tus.get(k)
|
||||
if not t or t["lo"] is None:
|
||||
continue
|
||||
if prev and prev[1] > t["lo"]:
|
||||
order_violations.append("%s (ends %06x) overlaps %s (starts %06x)"
|
||||
% (prev[0], prev[1], k, t["lo"]))
|
||||
prev = (k, t["hi"])
|
||||
|
||||
bt_total = sum(tus[k]["bytes"] for k in bt_keys if k in tus)
|
||||
btl4_total = sum(tus[k]["bytes"] for k in btl4_keys if k in tus)
|
||||
bt_funcs = sum(tus[k]["funcs"] for k in bt_keys if k in tus)
|
||||
btl4_funcs = sum(tus[k]["funcs"] for k in btl4_keys if k in tus)
|
||||
|
||||
hdr = ("| TU | survives | funcs | KB | addr range | assert-anchored | "
|
||||
"max assert line | recon @addr hits |\n|---|---|---|---|---|---|---|---|")
|
||||
"DP-inferred | max assert line | recon @addr hits |\n"
|
||||
"|---|---|---|---|---|---|---|---|---|")
|
||||
|
||||
with open(OUT_MD, "w") as f:
|
||||
with open(OUT_MD, "w", encoding="utf-8") as f:
|
||||
f.write("# BT 4.10 source manifest — measured from BTL4OPT.EXE\n\n")
|
||||
f.write("Generated by `tools/manifest410.py`. Census = the surviving "
|
||||
"makefiles (BT.MAK 36 TUs, BTL4.MAK 13 TUs + btl4.obj); "
|
||||
"attribution = in-body assert strings (resolved via "
|
||||
"section_dump), exporter file= tags, link-order fill.\n\n")
|
||||
"makefiles (BT.MAK 36 TUs, BTL4.MAK 13 TUs + btl4.obj, in "
|
||||
"LINK ORDER). Attribution = Fail() assert strings resolved "
|
||||
"via section_dump [T1 anchors] + CLASSMAP seed addresses "
|
||||
"[T1] + a monotone-in-link-order DP over data-section "
|
||||
"cluster affinity and recon @addr citations [T3 for "
|
||||
"unanchored boundaries]. A `—` row = TU NOT LOCALIZED: its "
|
||||
"code is contained inside an adjacent row's range (no "
|
||||
"anchor evidence yet); 8 such TUs survive as full .cpp "
|
||||
"anyway. `bt_l4/btl4.cpp` links at the image BOTTOM with "
|
||||
"the startup OBJ and is not part of this region.\n\n")
|
||||
f.write("Totals: **%d functions** in the image; BT-side: **%d funcs / "
|
||||
"%.0f KB** (bt.lib) + **%d funcs / %.0f KB** (btl4.lib+main); "
|
||||
"boundary-uncertain: %d; unattributed (outside any anchor "
|
||||
@@ -314,6 +516,10 @@ def main():
|
||||
f.write("\n## btl4.lib + main (link order)\n\n%s\n" % hdr)
|
||||
for k in btl4_keys:
|
||||
f.write(tbl_row(k) + "\n")
|
||||
if order_violations:
|
||||
f.write("\n## ⚠ Link-order violations (attribution suspects)\n\n")
|
||||
for v in order_violations:
|
||||
f.write("- %s\n" % v)
|
||||
f.write("\n## Engine/other TUs seen in the image (context only — "
|
||||
"their source survives)\n\n")
|
||||
f.write("| TU | funcs | KB |\n|---|---|---|\n")
|
||||
@@ -321,7 +527,7 @@ def main():
|
||||
t = tus[k]
|
||||
f.write("| %s | %d | %.1f |\n" % (k, t["funcs"], t["bytes"] / 1024.0))
|
||||
|
||||
with open(OUT_JSON, "w") as f:
|
||||
with open(OUT_JSON, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"census": CENSUS,
|
||||
"functions": [
|
||||
|
||||
Reference in New Issue
Block a user