summaryrefslogtreecommitdiff
path: root/src/eval_tune/bake_dna.py
blob: 9d036442eebec81f2d1a31e198932e2027c3abeb (plain)
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env python3
"""
Bake a tuned .dna file into eval.c as the new hardcoded baseline.

The .dna format (see ExportEvalDNA/ImportEvalDNA in eval.c) is
POSITIONAL: one line per entry in g_EvalDNA[], in that exact order,
each line a flat comma-separated list matching that entry's declared
size (DNA_VAR=1, DNA_ARRAY(x)=ARRAY_LENGTH(x),
DNA_MATRIX(x)=ARRAY_LENGTH(x)*ARRAY_LENGTH(x[0])). There are no names
in the file -- this script recovers the name/order from the
g_EvalDNA[] initializer in eval.c itself, so it only works against
the exact eval.c revision the .dna was tuned against. If arrays were
added/reordered/resized since, per-entry count checks below will
catch a misalignment and abort rather than silently writing values
into the wrong array.

This does NOT touch formatting/whitespace/line-breaks in eval.c's
board-shaped grids -- it walks each array's existing initializer,
replaces only the numeric literals in place (in textual order), and
leaves everything else (comments, 8x8 layout) untouched, so `git
diff` on a bake-in only shows the numbers that changed.
"""
import argparse
import re
import sys

DNA_ENTRY_RE = re.compile(r'DNA_(VAR|ARRAY|MATRIX)\((\w+)\)')
INT_RE = re.compile(r'-?\d+')
# Declarations seen for DNA-tracked values are `static SCORE x = ...`,
# `static ULONG x[...] = ...`, or (RACER_WINS_RACE) `SCORE x = ...`
# with no `static` at all -- match any of these.
DECL_RE_TMPL = r'(?:static\s+)?(?:SCORE|ULONG)\s+{name}\s*(\[[^=;]*\])?\s*=\s*'


def find_g_eval_dna_order(src):
    m = re.search(r'static DNA_BASE_SIZE g_EvalDNA\[\]\s*=\s*\{(.*?)\};',
                   src, re.S)
    if not m:
        sys.exit("could not find g_EvalDNA[] initializer in eval.c")
    return [(kind, name) for kind, name in DNA_ENTRY_RE.findall(m.group(1))]


def mask_comments(text):
    """Blank out //... and /*...*/ comment bodies, preserving length/newlines
    so character offsets into the masked text stay valid in the original."""
    out = list(text)
    i = 0
    n = len(text)
    while i < n:
        if text[i:i+2] == "//":
            j = text.find("\n", i)
            j = n if j == -1 else j
            for k in range(i, j):
                out[k] = " "
            i = j
        elif text[i:i+2] == "/*":
            j = text.find("*/", i + 2)
            j = n if j == -1 else j + 2
            for k in range(i, j):
                if out[k] != "\n":
                    out[k] = " "
            i = j
        else:
            i += 1
    return "".join(out)


def find_declaration_span(src, name):
    """Return (value_start, value_end, is_scalar) for the `... name ... = ...;` declaration."""
    m = re.search(DECL_RE_TMPL.format(name=re.escape(name)), src)
    if not m:
        sys.exit(f"could not find declaration of {name} in eval.c")
    value_start = m.end()
    if src[value_start:].lstrip().startswith("{"):
        # array/matrix: find the matching closing brace by depth counting
        i = src.index("{", value_start)
        depth = 0
        j = i
        while True:
            if src[j] == "{":
                depth += 1
            elif src[j] == "}":
                depth -= 1
                if depth == 0:
                    break
            j += 1
        semi = src.index(";", j)
        return i, semi, False
    else:
        semi = src.index(";", value_start)
        return value_start, semi, True


def replace_ints(text, new_values):
    """Replace only the numeric literals that are live code (not inside
    comments) with new_values, in order. Returns (new_text, matched, remaining)."""
    masked = mask_comments(text)
    matches = list(INT_RE.finditer(masked))
    if len(matches) != len(new_values):
        return text, len(matches), len(new_values) - len(matches)
    out = []
    pos = 0
    for m, val in zip(matches, new_values):
        out.append(text[pos:m.start()])
        out.append(str(val))
        pos = m.end()
    out.append(text[pos:])
    return "".join(out), len(matches), 0


def bake(eval_c_path, dna_path, out_path):
    src = open(eval_c_path).read()
    order = find_g_eval_dna_order(src)
    dna_lines = [l for l in open(dna_path).read().splitlines() if l.strip()]

    if len(dna_lines) != len(order):
        sys.exit(f"DNA file has {len(dna_lines)} lines but g_EvalDNA[] has "
                  f"{len(order)} entries -- eval.c and the .dna file don't "
                  f"match (different revision?). Aborting.")

    # Apply edits back-to-front by source position so earlier offsets
    # stay valid as we splice.
    edits = []
    for (kind, name), line in zip(order, dna_lines):
        values = [int(v) for v in line.split(",") if v.strip() != ""]
        start, end, is_scalar = find_declaration_span(src, name)
        segment = src[start:end]
        new_segment, matched, remaining = replace_ints(segment, values)
        if remaining != 0 or matched != len(values):
            sys.exit(f"{name}: declaration has {matched} numeric literals "
                      f"but .dna line has {len(values)} values -- aborting "
                      f"(likely eval.c/.dna mismatch).")
        edits.append((start, end, new_segment, name))

    edits.sort(key=lambda e: e[0], reverse=True)
    for start, end, new_segment, name in edits:
        src = src[:start] + new_segment + src[end:]

    with open(out_path, "w") as f:
        f.write(src)
    print(f"baked {len(order)} entries from {dna_path} into {out_path}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("dna_file")
    ap.add_argument("--eval-c", default="../eval.c")
    ap.add_argument("--out", default=None,
                     help="defaults to overwriting --eval-c in place")
    args = ap.parse_args()
    bake(args.eval_c, args.dna_file, args.out or args.eval_c)