Research source · python

verify_sat_boundary.py

site/public/research-artifacts/foundations/verify_sat_boundary.py

671 lines. Source is displayed for inspection; it is not executed by this page.

File fingerprint

SHA-256: c6a71ac0940b22dc3e333ba5c5cbca77a4106965b423ac8416ee0eb6e74309e3

#!/usr/bin/env python3
"""Finite corroboration, not a formal proof or a Python resource-cost claim."""
from dataclasses import dataclass
from itertools import combinations, permutations, product
from math import comb
from random import Random
from pathlib import Path
import hashlib
import json


def models(clauses, n):
    return {a for a in range(1 << n)
            if all(any(bool(a & (1 << (abs(l)-1))) == (l > 0) for l in c)
                   for c in clauses)}


def split_clause(c, nextvar, horn=False):
    c = tuple(c)
    if horn:
        assert sum(l > 0 for l in c) <= 1
        c = tuple(l for l in c if l < 0) + tuple(l for l in c if l > 0)
    if len(c) <= 3:
        return (c,), nextvar
    z = list(range(nextvar, nextvar + len(c)-3))
    out = [(c[0], c[1], z[0])]
    out += [(-z[j-1], c[j+1], z[j]) for j in range(1, len(z))]
    out += [(-z[-1], c[-2], c[-1])]
    return tuple(out), nextvar + len(z)


def synth_relation(b, relation):
    # Unsplit canonical CNF, used independently from the finite tables.
    return tuple(tuple((-(i+1) if a >> i & 1 else i+1) for i in range(b))
                 for a in range(1 << b) if not (relation >> a & 1))


def clause_mask(b, c):
    return sum(1 << a for a in models((c,), b))


def cnf_closure(b, horn=False):
    clauses = []
    for n in range(0, b+1 if horn else min(2, b)+1):
        for variables in combinations(range(1, b+1), n):
            for signs in product((-1, 1), repeat=n):
                c = tuple(v*s for v, s in zip(variables, signs))
                if not horn or sum(l > 0 for l in c) <= 1:
                    clauses.append(clause_mask(b, c))
    rels = {(1 << (1 << b))-1}
    for c in clauses:
        rels |= {r & c for r in rels}
    return rels


def affine_relations(b):
    rels = {(1 << (1 << b))-1}
    for row in range(1 << b):
        for rhs in (0, 1):
            c = sum(1 << a for a in range(1 << b)
                    if (a & row).bit_count() % 2 == rhs)
            rels |= {r & c for r in rels}
    return rels


def horn_protocol(b, left, right):
    """Communication accounting only; enumerating local models is free here."""
    forced = 0
    idle = 0
    turns = 0
    additions = 0
    bits = 0
    coordinate_bits = (max(1,b)-1).bit_length()
    while True:
        relation = left if turns % 2 == 0 else right
        eligible = [a for a in range(1<<b) if relation >> a & 1 and a & forced == forced]
        turns += 1
        bits += 2  # message type: failure, no change, or additions
        if not eligible:
            result = False
            break
        least = (1<<b)-1
        for a in eligible:
            least &= a
        assert relation >> least & 1  # meet closure, not true for arbitrary CNF
        new = (least ^ forced).bit_count()
        if not new:
            idle += 1
            if idle == 2:
                result = True
                break
        else:
            idle = 0
            additions += new
            bits += (coordinate_bits+1)*new+1  # delimited coordinate list
        forced = least
    assert turns <= 2*b+2 and additions <= b
    assert bits <= b*coordinate_bits+6*b+4
    return result, turns, bits


# Tables indexed by fixed slots. Relation bits use the sorted list of slots.
def lookup(mask, slots, assign):
    index = sum(assign[i] << j for j, i in enumerate(slots))
    return bool(mask >> index & 1)


def update(op, items):
    kind = op[0]
    if kind == 'E':
        slots = tuple(op[1])
        return slots, (1 << (1 << len(slots)))-1
    if kind == 'J':
        (s, a), (t, b) = items
        if s != t:
            raise ValueError('sort')
        return s, a & b
    s, r = items[0]
    if kind == 'I':
        if op[1] in s:
            raise ValueError('introduce')
        t = tuple(sorted(s + (op[1],)))
    elif kind == 'F':
        if op[1] not in s:
            raise ValueError('forget')
        t = tuple(i for i in s if i != op[1])
    elif kind == 'C':
        if not all(abs(l)-1 in s for l in op[1]):
            raise ValueError('clause')
        t = s
    elif kind == 'R':
        if tuple(sorted(op[1])) != tuple(range(len(op[1]))) or any(i >= len(op[1]) for i in s):
            raise ValueError('permutation')
        t = tuple(sorted(op[1][i] for i in s))
    else:
        raise ValueError('opcode')
    out = 0
    for a in range(1 << len(t)):
        assignment = {i: (a >> j) & 1 for j, i in enumerate(t)}
        if kind == 'F':
            ok = any(lookup(r, s, assignment | {op[1]: v}) for v in (0,1))
        elif kind == 'I':
            ok = lookup(r, s, assignment)
        elif kind == 'R':
            ok = lookup(r, s, {i: assignment[op[1][i]] for i in s})
        else:
            ok = lookup(r, s, assignment) and any(assignment[abs(l)-1] == (l > 0) for l in op[1])
        if ok:
            out |= 1 << a
    return t, out


@dataclass(frozen=True)
class Flat:
    n: int
    boundary: tuple  # pairs (slot, literal-variable-ID)
    clauses: tuple


def flat_step(op, fs):
    if op[0] == 'E':
        return Flat(len(op[1]), tuple((s, i+1) for i, s in enumerate(op[1])), ())
    f = fs[0]
    b = dict(f.boundary)
    if op[0] == 'I':
        b[op[1]] = f.n+1
        return Flat(f.n+1, tuple(sorted(b.items())), f.clauses)
    if op[0] == 'F':
        del b[op[1]]
        return Flat(f.n, tuple(sorted(b.items())), f.clauses)
    if op[0] == 'C':
        c = tuple((1 if l > 0 else -1)*b[abs(l)-1] for l in op[1])
        return Flat(f.n, f.boundary, f.clauses+(c,))
    if op[0] == 'R':
        return Flat(f.n, tuple(sorted((op[1][s],v) for s,v in f.boundary)), f.clauses)
    if op[0] == 'J':
        g = fs[1]
        remap = {v: b[s] for s, v in g.boundary}
        n = f.n
        for v in range(1, g.n+1):
            if v not in remap:
                n += 1
                remap[v] = n
        gc = tuple(tuple((1 if l > 0 else -1)*remap[abs(l)] for l in c) for c in g.clauses)
        return Flat(n, f.boundary, f.clauses+gc)
    raise ValueError(op)


def flat_relation(f):
    b = tuple(sorted(f.boundary))
    out = 0
    for a in models(f.clauses, f.n):
        image = sum(((a >> (v-1)) & 1) << j for j, (_,v) in enumerate(b))
        out |= 1 << image
    return tuple(s for s, _ in b), out


@dataclass(frozen=True, eq=False)
class Node:
    op: tuple
    args: tuple
    tag: tuple


def node(op, *args):
    return Node(op, args, update(op, [x.tag for x in args]))


def decode(n):
    return flat_step(n.op, [decode(x) for x in n.args])


def postfix(n):
    return [op for child in n.args for op in postfix(child)] + [n.op]


def abstract_stack(program):
    st = []
    for op in program:
        arity = 0 if op[0] == 'E' else 2 if op[0] == 'J' else 1
        if len(st) < arity:
            return None
        try:
            args = st[-arity:] if arity else []
            v = update(op, args)
        except ValueError:
            return None
        if arity:
            del st[-arity:]
        st.append(v)
    return st


def abstract_program(program):
    st = abstract_stack(program)
    return st is not None and len(st) == 1 and st[0] == ((), 1)


class RecordMachine:
    """Literal immutable binary records; only read/copy paths may reach old nodes.

    Python allocation and finite-table evaluation are not charged machine time.
    The audit records old target paths, labels, batch size, and immutability.
    """
    def __init__(self):
        self.heap = [(('bottom',), ())]
        self.root = 0
        self.bad = False
        self.accept = False
        self.max_radius = 0
        self.max_batch = 0

    def old(self, path):
        assert len(path) <= 2
        self.max_radius = max(self.max_radius, len(path))
        v = self.root
        for p in path:
            v = self.heap[v][1][p]
        return v

    def step(self, op):
        if self.bad:
            return
        try:
            label, _ = self.heap[self.old(())]
            kind = op[0]
            if kind == 'E':
                args = ()
                tag = update(op, [])
                tail = self.old(())
                first = label[0] == 'bottom'
            elif kind == 'J':
                if label[0] != 'stack' or label[1]:
                    raise ValueError('underflow')
                right = self.old((0,))
                left = self.old((1,0))
                tag = update(op, [self.heap[left][0][2], self.heap[right][0][2]])
                args = (left, right)
                first = self.heap[self.old((1,))][0][1]
                tail = self.old((1,1))
            else:
                if label[0] != 'stack':
                    raise ValueError('underflow')
                value = self.old((0,))
                args = (value,)
                tag = update(op, [self.heap[value][0][2]])
                tail = self.old((1,))
                first = label[1]
        except ValueError:
            self.bad = True
            self.accept = False
            return
        old_length = len(self.heap)
        # A simultaneous two-record batch, with its stack pointing to its value.
        self.heap.extend([(('value',op,tag),args), (('stack',first),(old_length,tail))])
        self.root = old_length+1
        self.max_batch = max(self.max_batch, len(self.heap)-old_length)
        self.accept = first and tag == ((),1)

    def stack_tags(self, root):
        # An uncharged independent verifier, not part of the local transition.
        result = []
        while self.heap[root][0][0] != 'bottom':
            value, root = self.heap[root][1]
            result.append(self.heap[value][0][2])
        return result[::-1]


def check_records(program):
    m = RecordMachine()
    retained = [(m.root, [])]
    for i, op in enumerate(program):
        old_heap = tuple(m.heap)
        m.step(op)
        assert tuple(m.heap[:len(old_heap)]) == old_heap
        expected = abstract_stack(program[:i+1])
        assert m.bad == (expected is None)
        assert m.accept == abstract_program(program[:i+1])
        if expected is not None:
            assert m.stack_tags(m.root) == expected
            retained.append((m.root, expected))
    for root, expected in retained:
        assert m.stack_tags(root) == expected
    assert m.max_radius <= 2 and m.max_batch <= 2
    assert all(len(pointers) <= 2 for _, pointers in m.heap)
    return m.max_radius, m.max_batch


BOTTOM = 'bottom'
BAD = 'bad'


def stack_transition(ctrl, op, top):
    """Finite control and just one old stack top determine this transition."""
    kind = op[0]
    action = 'push' if kind == 'E' else 'pop' if kind == 'J' else 'keep'
    if ctrl == BAD:
        return BAD, action, BOTTOM
    try:
        if kind == 'E':
            return (update(op,[]), ctrl is None), 'push', BOTTOM if ctrl is None else ctrl
        if ctrl is None:
            raise ValueError('underflow')
        if kind == 'J':
            if top in (None,BOTTOM):
                raise ValueError('underflow')
            return (update(op,[top[0],ctrl[0]]),top[1]), 'pop', None
        return (update(op,[ctrl[0]]),ctrl[1]), 'keep', None
    except ValueError:
        return BAD, action, BOTTOM


def check_single_edge(program):
    # Instantiate the checked compiler's three rows; base is unlabelled.
    nodes = [(None,None)]
    top = 0
    ctrl = None
    auxiliary = []
    for i, op in enumerate(program):
        label, edge = nodes[top]
        assert label == (auxiliary[-1] if auxiliary else None)
        ctrl, action, value = stack_transition(ctrl,op,label)
        if action == 'push':
            new = (value,top)
            auxiliary.append(value)
        elif action == 'keep':
            new = (label,edge)
        else:
            new = nodes[edge] if edge is not None else (None,None)
            if auxiliary:
                auxiliary.pop()
        nodes.append(new)
        top = len(nodes)-1
        # Universal decoder is supplied by Compile.lean; these are finite checks.
        decoded = []
        cursor = top
        while cursor is not None and nodes[cursor][0] is not None:
            decoded.append(nodes[cursor][0])
            cursor = nodes[cursor][1]
        assert decoded[::-1] == auxiliary
        expected = abstract_stack(program[:i+1])
        assert (ctrl == BAD) == (expected is None)
        accept = ctrl not in (None,BAD) and ctrl[1] and ctrl[0] == ((),1)
        assert accept == abstract_program(program[:i+1])
        if expected is not None:
            assert auxiliary and auxiliary[0] == BOTTOM
            assert [entry[0] for entry in auxiliary[1:]]+[ctrl[0]] == expected
            assert ctrl[1] == (len(expected) == 1)
    assert len(nodes) == len(program)+1
    return len(program)


class TapeMachine:
    """One scanned cell, at most one write and one head move per input token."""
    def __init__(self):
        self.tape = {}  # all blank, including position zero
        self.head = 0
        self.phase = 'ready'
        self.pending = None
        self.accept = False
        self.steps = 0
        self.max_move = 0

    def step(self, symbol):
        self.steps += 1
        cell = self.tape.get(self.head)
        before = self.head
        move = 0
        write = cell
        self.accept = False
        if self.phase == 'bad':
            return
        try:
            if self.phase == 'ready':
                if symbol == '#':
                    raise ValueError('unexpected clock')
                kind = symbol[0]
                if kind == 'E':
                    self.pending = (update(symbol, []), cell is None)
                    move = 1
                    self.phase = 'push'
                elif kind == 'J':
                    if cell is None or cell[1]:
                        raise ValueError('underflow')
                    self.pending = cell[0]
                    write = None
                    move = -1
                    self.phase = 'join'
                else:
                    if cell is None:
                        raise ValueError('underflow')
                    write = (update(symbol, [cell[0]]), cell[1])
                    self.phase = 'idle'
            else:
                if symbol != '#':
                    raise ValueError('missing clock')
                if self.phase == 'push':
                    write = self.pending
                elif self.phase == 'join':
                    if cell is None:
                        raise ValueError('underflow')
                    write = (update(('J',), [cell[0], self.pending]), cell[1])
                self.phase = 'ready'
                self.pending = None
                self.accept = write is not None and write[1] and write[0] == ((),1)
        except ValueError:
            self.phase = 'bad'
            move = 0
            write = cell
        self.tape[self.head] = write
        self.head += move
        self.max_move = max(self.max_move, abs(self.head-before))
        assert self.head >= 0


def encode(program):
    return [s for op in program for s in (op, '#')]


def reference_clock(word):
    if len(word) % 2 or any(word[i] == '#' or word[i+1] != '#' for i in range(0,len(word),2)):
        return False
    return abstract_program(word[::2])


def check_machine(word):
    m = TapeMachine()
    for i, symbol in enumerate(word):
        m.step(symbol)
        assert m.accept == reference_clock(word[:i+1])
    assert m.accept == reference_clock(word)
    assert m.steps == len(word) and m.max_move <= 1
    return m.steps


def run():
    out = {'scope': 'Finite Boolean and literal TM model checks. No proof assistant, no arbitrary-heap verification, no claim that Python data-structure costs satisfy record bounds.'}
    out['bounds'] = {
        'type_counts_and_pins': 'all Boolean relations at b=0,1,2,3',
        'gadgets': 'all literal sign patterns at lengths 4 through 7, plus all Horn positive-literal positions',
        'retained_dags': '1000 seeded attempts, w=2, at most 9 decoded variables and 45 postfix operations; seed 20261001',
        'postfix_words': 'all words through length 4 over the seven listed test opcodes',
        'postfix_test_alphabet': ['Empty_empty','Empty_slot0','Introduce0','Forget0','Add_x0','Add_not_x0','Join'],
        'tm': 'one scanned cell and one move at most per encoded symbol; every tested prefix; initially all blank',
        'record_model': 'two pointer fields, two fresh records, old endpoint paths of length at most two; all earlier tested stacks remain intact',
        'single_edge_model': 'one finite-control stack evaluator and the push/keep/pop scaffold rows, one appended node per unpadded opcode; prefix decoding and acceptance checked',
        'shared_implementation': 'TM and record transitions share finite relation-update tables with the abstract stack; independent CNF enumeration checks table semantics on the stated samples',
        'omitted': 'arbitrary widths/heaps/histories, simultaneous-record compiler execution, generic TM-to-scaffold instantiation, proof-assistant verification and optimality'
    }
    relation_checks = 0
    for b in range(4):
        for r in range(1 << (1 << b)):
            assert sum(1 << a for a in models(synth_relation(b,r),b)) == r
            relation_checks += 1
    out['all_relation_syntheses_b_0_to_3'] = relation_checks
    renames = 0
    for w in range(4):
        for length in range(w+1):
            for slots in combinations(range(w), length):
                for r in range(1 << (1 << length)):
                    f = Flat(length, tuple((s,i+1) for i,s in enumerate(slots)), synth_relation(length,r))
                    for pi in permutations(range(w)):
                        op = ('R',pi)
                        assert update(op, [(slots,r)]) == flat_relation(flat_step(op,[f]))
                        inverse = tuple(pi.index(i) for i in range(w))
                        assert update(('R',inverse), [update(op, [(slots,r)])]) == (slots,r)
                        renames += 1
    out['renaming_checks_all_partial_sorts_w_0_to_3'] = renames
    gadget_checks = 0
    for b in range(4,8):
        for signs in product((-1,1), repeat=b):
            c = tuple(s*(i+1) for i,s in enumerate(signs))
            clauses, nextvar = split_clause(c,b+1)
            projected = {a & ((1<<b)-1) for a in models(clauses,nextvar-1)}
            assert projected == models((c,),b)
            assert max(map(len, clauses)) <= 3
            gadget_checks += 1
        for positive in range(-1,b):
            c = tuple((i+1) if i == positive else -(i+1) for i in range(b))
            clauses, nextvar = split_clause(c,b+1,True)
            assert all(sum(l>0 for l in c) <= 1 for c in clauses)
            assert {a & ((1<<b)-1) for a in models(clauses,nextvar-1)} == models((c,),b)
            gadget_checks += 1
    out['exhaustive_clause_gadgets_lengths_4_to_7'] = gadget_checks
    counts = []
    for b in range(4):
        binary = cnf_closure(b)
        affine = affine_relations(b)
        horn = cnf_closure(b,True)
        # Independent closure characterizations for every Boolean relation.
        majority = set(); meets = set(); parity = set()
        for r in range(1 << (1<<b)):
            vals = [a for a in range(1<<b) if r >> a & 1]
            if all(r >> (a & c) & 1 for a in vals for c in vals):
                meets.add(r)
            if all(r >> ((a & c)|(a & d)|(c & d)) & 1 for a in vals for c in vals for d in vals):
                majority.add(r)
            if all(r >> (a ^ c ^ d) & 1 for a in vals for c in vals for d in vals):
                parity.add(r)
        assert binary == majority and horn == meets and affine == parity
        counts.append({'b':b,'all':1<<(1<<b),'2cnf':len(binary),'affine':len(affine),'horn':len(horn)})
    out['exhaustive_small_type_counts'] = counts
    communication_checks = []
    for b in range(4):
        relations = cnf_closure(b,True)
        checks = max_turns = max_bits = 0
        for left in relations:
            for right in relations:
                answer, turns, bits = horn_protocol(b,left,right)
                assert answer == bool(left & right)
                max_turns = max(max_turns,turns)
                max_bits = max(max_bits,bits)
                checks += 1
        communication_checks.append({'b':b,'pairs':checks,'max_turns':max_turns,'max_communicated_bits':max_bits})
    out['horn_protocol_all_relation_pairs_b_0_to_3'] = communication_checks
    out['horn_middle_layer_families'] = []
    for b in range(2,5):
        t = b//2
        layer = [a for a in range(1<<b) if a.bit_count() == t]
        relations = set()
        for x in range(1<<len(layer)):
            selected = {a for j,a in enumerate(layer) if x >> j & 1}
            clauses = [tuple(-(i+1) for i in range(b) if a >> i & 1)
                       for a in range(1<<b) if (a.bit_count() == t and a not in selected) or a.bit_count() == t+1]
            actual = models(clauses,b)
            expected = {a for a in range(1<<b) if a.bit_count() < t} | selected
            assert actual == expected
            relations.add(tuple(sorted(actual)))
        assert len(relations) == 1<<comb(b,t)
        out['horn_middle_layer_families'].append({'b':b,'distinct':len(relations)})
    # Pins distinguish all pairs for b<=3; bit arithmetic is an independent oracle.
    pins = 0
    for b in range(4):
        for r in range(1 << (1<<b)):
            for t in range(r):
                a = ((r ^ t) & -(r ^ t)).bit_length()-1
                pinclauses = tuple(((i+1) if a >> i & 1 else -(i+1),) for i in range(b))
                pin = sum(1<<v for v in models(pinclauses,b))
                assert bool(r & pin) != bool(t & pin)
                full = (1 << (1<<b))-1
                assert not r & (full ^ r)
                assert r & (full ^ t) or t & (full ^ r)
                pins += 1
    out['distinguishing_pin_pairs'] = pins
    out['disjointness_fooling_pair_checks'] = pins
    # Retained random constructor DAGs; full CNF decoding uses separate witness enumeration.
    rng = Random(20261001)
    pool = [node(('E',s)) for s in ((),(0,),(1,),(0,1))]
    checks = oldchecks = 0
    machine_steps = 0
    programs = []
    record_results = []
    single_edge_steps = 0
    for _ in range(1000):
        a = rng.choice(pool)
        s = a.tag[0]
        options = [('F',i) for i in s] + [('I',i) for i in (0,1) if i not in s]
        options += [('C',(sign*(i+1),)) for i in s for sign in (-1,1)]
        options += [('R',(1,0))]
        if len(s) == 2:
            options += [('C',c) for c in ((1,2),(-1,2),(1,-2),(-1,-2))]
        peers = [p for p in pool if p.tag[0] == s]
        op = rng.choice(options+[('J',)])
        args = (a,rng.choice(peers)) if op[0] == 'J' else (a,)
        h = node(op,*args)
        f = decode(h)
        prog = postfix(h)
        if f.n > 9 or len(prog) > 45:
            continue
        assert h.tag == flat_relation(f)
        checks += 1
        pool.append(h)
        old = rng.choice(pool)
        assert old.tag == flat_relation(decode(old))
        oldchecks += 1
        programs.append(prog)
        record_results.append(check_records(prog))
        single_edge_steps += check_single_edge(prog)
        machine_steps += check_machine(encode(prog))
    out['retained_dag_new_checks'] = checks
    out['retained_old_version_checks'] = oldchecks
    # Explicit private-name hygiene negative control, including zero boundary.
    f = Flat(1,(),((1,),)); g = Flat(1,(),((-1,),))
    assert flat_relation(flat_step(('J',),(f,g))) == ((),1)
    assert not models(f.clauses+g.clauses,1)
    # Positive-only observers miss a distinction visible to negative pins.
    assert models(((1,),),1) != models((),1)
    assert models(((1,),),1) & models(((1,),),1)
    assert not (models(((1,),),1) & models(((-1,),),1))
    out['negative_controls'] = ['private-name capture changes SAT','positive-only observations omit a pin distinction']
    alphabet = [('E',()),('E',(0,)),('I',0),('F',0),('C',(1,)),('C',(-1,)),('J',)]
    exhaustive = 0
    for length in range(5):
        for p in product(alphabet,repeat=length):
            record_results.append(check_records(p))
            single_edge_steps += check_single_edge(p)
            machine_steps += check_machine(encode(p))
            exhaustive += 1
    malformed = 0
    for p in programs[:150]:
        w = encode(p)
        variants = [w[:-1],w[:-2],['#']+w,w+['#']]
        if len(w) >= 3:
            variants += [w[:1]+w[2:], [w[0],('E',()),*w[2:]]]
        for v in variants:
            machine_steps += check_machine(v)
            malformed += 1
    out['clocked_programs_exhaustive_through_4_operations'] = exhaustive
    out['clock_mutations'] = malformed
    out['literal_tape_transitions_checked'] = machine_steps
    out['literal_record_programs_checked'] = len(record_results)
    out['single_edge_scaffold_appends_checked'] = single_edge_steps
    out['maximum_old_endpoint_radius_observed'] = max(r for r,_ in record_results)
    out['maximum_fresh_records_observed'] = max(a for _,a in record_results)
    out['universal_status'] = 'Theorems, view inequality and resource bounds are written proofs. The single-edge compiler has finite instances checked here and a prior Lean proof. This program does not certify unrestricted theorems, generic compiler correctness, or optimality.'
    root = Path(__file__).resolve().parents[3]
    paths = [
        'public/research-artifacts/foundations/sat-boundary-dynamics.md',
        'public/research-artifacts/foundations/verify_sat_boundary.py',
        'public/research-artifacts/foundations/foundational-contracts.md',
        'public/formal/persistent-stacks/lean/FPRDTransfer/RealtimeTransfer.lean',
        'public/formal/persistent-stacks/lean/FPRDTransfer/TapeZipper.lean',
        'public/formal/persistent-stacks/lean/FPRDTransfer/StackMachine.lean',
        'public/formal/persistent-stacks/lean/FPRDTransfer/Compile.lean',
    ]
    out['source_sha256'] = {p: hashlib.sha256((root/p).read_bytes()).hexdigest() for p in paths}
    return out


if __name__ == '__main__':
    print(json.dumps(run(),indent=2))