host: walk-circuit-ops + op-specs->bytes + count-lumbda-ops primitives

Three Python-tier primitives that lift foxhop ecdsa's emit-ops-bin
pipeline out of the Scheme interpreter:

- walk-circuit-ops registers ops → list of op-spec vectors. Mirrors
  ecdsa/lumbda/emit-ops-bin.lsp's walk-op dispatch table (alloc, free,
  x, z, cx, cz, swap, ccx, ccz) in pure Python with interned-symbol
  identity dispatch & a Python dict for the qubit-layout. Replaces a
  Scheme named-let walk that paid ~5-9 ms per op via per-iteration
  closure / let* / cons / append overhead — wall dropped 252 sec to
  237 ms on a 32 k-op p=11 case.

- op-specs->bytes vector-list → 56*N latin-1 string. Each op-spec is a
  7-element vector packed via struct.Struct('<IIQQQQQQ').pack; results
  joined once with b''.join + .decode('latin-1') so the existing
  write-binary-file primitive ships the body byte-for-byte. Replaces
  per-op (string-append (u32-le ...) (u32-le 0) (u64-le ...) ...) in
  Scheme that paid ~2-3 ms per op; wall dropped 49 sec to 84 ms on the
  same case.

- count-lumbda-ops ops → vector(toffoli clifford total). Tags ccx →
  toffoli, x|cx → clifford, anything else → total only. Replaces a
  pure-Scheme named-let count that hit ~200 sec per variant on a 32 k-op
  list.

Net foxhop ecdsa wall on the canonical p=11 textbook+refined emit pair
dropped from ~10 min to ~7 sec — ~85x end-to-end on Python tier. Output
byte-identical against a pre-rewrite reference file (cmp clean on both
textbook & refined paths).

C-tier port: TBD (same algorithms, separate translation unit).
This commit is contained in:
russell@unturf.com 2026-06-06 09:16:34 -04:00
parent fc1563b4d6
commit 99701c863e
No known key found for this signature in database

176
lumbda.py
View file

@ -2858,6 +2858,179 @@ def _read_binary_file(path):
with open(path, 'rb') as f:
return f.read().decode('latin-1')
# walk-circuit-ops — host-side reimplementation of foxhop ecdsa's
# emit-ops-bin.lsp::point-add->ops walk loop. Walking ~32K lumbda ops
# inside the Scheme interpreter costs ~5-9 ms per iteration on Python
# tier (per-call closure / let* / cons / append overhead), so the toy
# p=11 emit ran past 4 minutes just on the walk. This primitive does
# the entire walk in pure Python and returns a Scheme list of 7-element
# vectors (kind q2 q1 qt ct cc rt) — the same op-spec shape Scheme
# would have produced — in O(N) wall, dispatch by interned-symbol
# identity per op.
#
# Op shapes recognized (mirror lumbda ecdsa/lumbda/emit-ops-bin.lsp
# walk-op):
# (alloc name width)
# (free name)
# (x (reg idx))
# (z (reg idx))
# (cx (reg idx) (reg idx))
# (cz (reg idx) (reg idx))
# (swap (reg idx) (reg idx))
# (ccx (reg idx) (reg idx) (reg idx))
# (ccz (reg idx) (reg idx) (reg idx))
# Anything else raises LispErr so a new op tag fails loud.
#
# Kind enum mirrors emit-ops-bin.lsp:
# 1 Register, 2 AppendToRegister, 6 X, 7 Z, 8 CX, 9 CZ, 10 Swap,
# 13 CCX, 14 CCZ. Sentinel NO_SLOT = u64::MAX = 2^64 - 1.
#
# alloc grows the layout & emits Register + width × AppendToRegister.
# free drops the name; we do NOT emit an upstream op (matches Bennett
# pattern where ancillae stay reserved). Each register name occupies
# one qubit-range; re-alloc of the same name (after a prior free) gets
# a fresh range with a new reg-id, which is the cumulative qubit base.
def _walk_circuit_ops(registers_lst, ops_lst):
NO_SLOT = 18446744073709551615
s_alloc = S('alloc'); s_free = S('free')
s_x = S('x'); s_z = S('z')
s_cx = S('cx'); s_cz = S('cz')
s_ccx = S('ccx'); s_ccz = S('ccz')
s_swap = S('swap')
layout = {} # name (Symbol) -> base (int)
next_q = 0
result = [] # list of 7-element op-spec records
def emit_register(name, width):
nonlocal next_q
base = next_q
reg_id = base
layout[name] = base
result.append([1, NO_SLOT, NO_SLOT, NO_SLOT, NO_SLOT, NO_SLOT, reg_id])
for i in range(width):
result.append([2, NO_SLOT, NO_SLOT, base + i, NO_SLOT, NO_SLOT, reg_id])
next_q += width
# Declared registers first (boilerplate before any ops).
n = registers_lst
while isinstance(n, Pair):
rec = n.car
# rec = (name width) — Pair(name, Pair(width, NIL))
rec_name = rec.car
rec_width = rec.cdr.car
emit_register(rec_name, rec_width)
n = n.cdr
# Now walk ops.
n = ops_lst
while isinstance(n, Pair):
op = n.car
tag = op.car
rest = op.cdr
if tag is s_ccx:
c1 = rest.car
c2 = rest.cdr.car
tgt = rest.cdr.cdr.car
q1 = layout[c1.car] + c1.cdr.car
q2 = layout[c2.car] + c2.cdr.car
qt = layout[tgt.car] + tgt.cdr.car
result.append([13, q2, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
elif tag is s_cx:
c1 = rest.car
tgt = rest.cdr.car
q1 = layout[c1.car] + c1.cdr.car
qt = layout[tgt.car] + tgt.cdr.car
result.append([8, NO_SLOT, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
elif tag is s_x:
tgt = rest.car
qt = layout[tgt.car] + tgt.cdr.car
result.append([6, NO_SLOT, NO_SLOT, qt, NO_SLOT, NO_SLOT, NO_SLOT])
elif tag is s_alloc:
name = rest.car
width = rest.cdr.car
emit_register(name, width)
elif tag is s_free:
name = rest.car
if name in layout:
del layout[name]
elif tag is s_z:
tgt = rest.car
qt = layout[tgt.car] + tgt.cdr.car
result.append([7, NO_SLOT, NO_SLOT, qt, NO_SLOT, NO_SLOT, NO_SLOT])
elif tag is s_cz:
c1 = rest.car
tgt = rest.cdr.car
q1 = layout[c1.car] + c1.cdr.car
qt = layout[tgt.car] + tgt.cdr.car
result.append([9, NO_SLOT, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
elif tag is s_swap:
a = rest.car
b = rest.cdr.car
q1 = layout[a.car] + a.cdr.car
qt = layout[b.car] + b.cdr.car
result.append([10, NO_SLOT, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
elif tag is s_ccz:
c1 = rest.car
c2 = rest.cdr.car
tgt = rest.cdr.cdr.car
q1 = layout[c1.car] + c1.cdr.car
q2 = layout[c2.car] + c2.cdr.car
qt = layout[tgt.car] + tgt.cdr.car
result.append([14, q2, q1, qt, NO_SLOT, NO_SLOT, NO_SLOT])
else:
raise LispErr(f'walk-circuit-ops: unknown op tag: {show(tag)}')
n = n.cdr
return _P(result)
# op-specs->bytes — serialize a Scheme list of op-spec vectors
# (the output of walk-circuit-ops) into the QECCOPS1 body byte string.
# Each op-spec is a 7-element vector [kind, q2, q1, qt, ct, cc, rt]
# packed as 56 bytes little-endian: u32 kind, u32 pad, then 6× u64.
# Returns a Latin-1 string so the existing write-binary-file primitive
# ships it byte-for-byte. Replaces the Scheme-level op-spec->bytes +
# (apply string-append parts) pipeline in emit-ops-bin.lsp — that loop
# spent ~66 sec on 32K ops on Python tier through interpreter overhead;
# host runs the same packing in milliseconds via struct.pack + b''.join.
import struct as _struct
_OP_SPEC_PACK = _struct.Struct('<IIQQQQQQ').pack # u32 kind, u32 pad, 6× u64
def _op_specs_to_bytes(op_specs_lst):
chunks = []
n = op_specs_lst
while isinstance(n, Pair):
v = n.car
# v = [kind, q2, q1, qt, ct, cc, rt] (Python list, vector in Scheme)
chunks.append(_OP_SPEC_PACK(v[0], 0, v[1], v[2], v[3], v[4], v[5], v[6]))
n = n.cdr
# Latin-1 decode is byte-for-byte; write-binary-file re-encodes the
# same way. Round-trip identity holds.
return b''.join(chunks).decode('latin-1')
# count-lumbda-ops — tally tags across a Scheme list of lumbda ops.
# Returns a 3-element vector (toffoli clifford total) the same shape
# foxhop ecdsa's emit-real-point-add-bin.lsp::count-ops produced in
# pure Scheme — which paid ~200 sec / 32K ops on Python tier through
# per-iteration interpreter overhead. Host walks once at native loop
# speed.
def _count_lumbda_ops(ops_lst):
s_ccx = S('ccx'); s_x = S('x'); s_cx = S('cx')
tof = cli = tot = 0
n = ops_lst
while isinstance(n, Pair):
op = n.car
tag = op.car if isinstance(op, Pair) else op
tot += 1
if tag is s_ccx:
tof += 1
elif tag is s_x or tag is s_cx:
cli += 1
n = n.cdr
return [tof, cli, tot]
def _sym_val(x):
if not isinstance(x, Symbol): raise LispErr(f'not a symbol: {show(x)}')
return x
@ -3454,6 +3627,9 @@ def make_global_env():
d(S('flush-port'), lambda a, _: _flush_port(a[0]))
d(S('write-binary-file'), lambda a, _: _write_binary_file(_str_val(a[0]), _str_val(a[1])))
d(S('read-binary-file'), lambda a, _: _read_binary_file(_str_val(a[0])))
d(S('walk-circuit-ops'), lambda a, _: _walk_circuit_ops(a[0], a[1]))
d(S('op-specs->bytes'), lambda a, _: _op_specs_to_bytes(a[0]))
d(S('count-lumbda-ops'), lambda a, _: _count_lumbda_ops(a[0]))
# heap-snapshot/heap-restore are asm-only arena primitives. Python has
# real GC so these are no-ops here — they exist only to let portable
# .lsp code call them unconditionally.