;;; sim.lsp — Reversible bit-level simulator for our S-expression circuits. ;;; ;;; Reads a circuit portal, executes its op stream on classical bits, ;;; counts Toffoli & Clifford gates & peak live qubits, runs a reverse ;;; pass to confirm reversibility, writes a result portal. ;;; ;;; Runs on every lumbda tier — Python, C, asm — byte-identically. ;;; ;;; Public entry point: (simulate-portal in-path out-path) ;;; ;;; Wire format documented in gates.lsp. ;;; ── tiny utilities ──────────────────────────────────────────── (define (read-portal filename) "Slurp a portal as a single S-expression. Uses file->string + read-from-string for cross-tier portability — (read port) on a raw file handle falls back to stdin on lumbda's Python tier." (read-from-string (file->string filename))) (define (write-portal! filename sexp) "Write an S-expression to a file atomically." (let ((tmp (string-append filename ".tmp"))) (let ((port (open-output-file tmp))) (write sexp port) (newline port) (close-port port)) (rename-file tmp filename))) (define (section sexp tag) "Return body (everything past tag) of first child whose head = tag, else #f." (let loop ((children (cdr sexp))) (cond ((null? children) #f) ((and (pair? (car children)) (eq? (car (car children)) tag)) (cdr (car children))) (else (loop (cdr children)))))) (define (int->bits n width) "Decode integer n into a vector of width bits, LSB at index 0." (let ((v (make-vector width 0))) (let loop ((i 0) (rest n)) (if (= i width) v (begin (vector-set! v i (remainder rest 2)) (loop (+ i 1) (quotient rest 2))))))) (define (bits->int v) "Encode a bit vector (LSB at index 0) back into an integer." (let loop ((i 0) (acc 0) (pow 1)) (if (= i (vector-length v)) acc (loop (+ i 1) (+ acc (* (vector-ref v i) pow)) (* pow 2))))) (define (vector-all-zero? v) (let loop ((i 0)) (cond ((= i (vector-length v)) #t) ((not (= (vector-ref v i) 0)) #f) (else (loop (+ i 1)))))) (define (vector-equal? a b) (and (= (vector-length a) (vector-length b)) (let loop ((i 0)) (cond ((= i (vector-length a)) #t) ((not (= (vector-ref a i) (vector-ref b i))) #f) (else (loop (+ i 1))))))) (define (vector-copy v) (let* ((n (vector-length v)) (out (make-vector n 0))) (let loop ((i 0)) (if (= i n) out (begin (vector-set! out i (vector-ref v i)) (loop (+ i 1))))))) ;;; ── simulator state ─────────────────────────────────────────── ;;; ;;; State carries: ;;; regs hash-table: register-name → bit vector (mutable) ;;; widths hash-table: register-name → declared width ;;; live integer: sum of current register widths ;;; peak integer: max live ever reached ;;; toffoli integer: count of ccx ops ;;; clifford integer: count of x + cx ops (define (make-sim-state) (vector (make-hash-table) ; 0: regs (make-hash-table) ; 1: widths 0 ; 2: live 0 ; 3: peak 0 ; 4: toffoli 0 ; 5: clifford 'running ; 6: status (running | leaked-ancilla | bad-op) (make-hash-table) ; 7: bits (Tier-2 classical bits, int-id → 0/1) '() ; 8: cond-stack (Tier-2 push-cond / pop-cond) 1)) ; 9: cond (Tier-2, current conditional mask, 0 or 1) (define (st-regs s) (vector-ref s 0)) (define (st-widths s) (vector-ref s 1)) (define (st-live s) (vector-ref s 2)) (define (st-peak s) (vector-ref s 3)) (define (st-toffoli s) (vector-ref s 4)) (define (st-clifford s) (vector-ref s 5)) (define (st-status s) (vector-ref s 6)) (define (st-bits s) (vector-ref s 7)) (define (st-cond-stack s) (vector-ref s 8)) (define (st-cond s) (vector-ref s 9)) (define (set-live! s v) (vector-set! s 2 v)) (define (set-peak! s v) (vector-set! s 3 v)) (define (set-toffoli! s v) (vector-set! s 4 v)) (define (set-clifford! s v) (vector-set! s 5 v)) (define (set-status! s v) (vector-set! s 6 v)) (define (set-cond-stack! s v) (vector-set! s 8 v)) (define (set-cond! s v) (vector-set! s 9 v)) (define (bit-ref s id) "Read classical bit; defaults to 0 if not yet set." (let ((bits (st-bits s))) (cond ((hash-table-exists? bits id) (hash-table-ref bits id)) (else 0)))) (define (bit-set! s id v) (hash-table-set! (st-bits s) id v)) (define (st-ok? s) (eq? (st-status s) 'running)) (define (touch-peak! s) (when (> (st-live s) (st-peak s)) (set-peak! s (st-live s)))) ;;; ── register initialization ─────────────────────────────────── (define (declare-registers! s registers-section input-section) ;; registers-section = ((target-x 4) (target-y 4) ...) ;; input-section = ((target-x 11) (target-y 6) ...) (for-each (lambda (rec) (let ((name (car rec)) (width (car (cdr rec)))) (hash-table-set! (st-widths s) name width) (let ((initial 0)) ;; lookup input value if present (let ((row (assoc name input-section))) (when row (set! initial (car (cdr row))))) (hash-table-set! (st-regs s) name (int->bits initial width))) (set-live! s (+ (st-live s) width)))) registers-section) (touch-peak! s)) ;;; ── op execution (forward and reverse) ──────────────────────── (define (qref->bit s ref) "ref = (reg-name idx)." (let ((reg (hash-table-ref (st-regs s) (car ref)))) (vector-ref reg (car (cdr ref))))) (define (qref-flip! s ref) (let* ((reg (hash-table-ref (st-regs s) (car ref))) (i (car (cdr ref))) (cur (vector-ref reg i))) (vector-set! reg i (- 1 cur)))) (define (exec-op! s op forward?) "Apply one op. forward? = #t for forward pass, #f for reverse. All Phase B Tier-1 gates {x, z, cx, cz, ccx, ccz, r} are self-inverse on classical-bit-valued single-shot state (z/cz/ccz add no observable bit-value effect; r unconditionally zeros target). Tier-2 ops {bit-invert, bit-store0, bit-store1, hmr, push-cond, pop-cond} require reversibility-aware handling — see sweep-013 RESULTS.md for the classical-replay design." (case (car op) ((x) ;; sweep-041: gate on st-cond to match HEAD's sim_cpu.c (line 62): ;; uint64_t v = cond ...; qubits[tgt] ^= v. Under push-cond=0 the ;; gate must NOT fire. Tier-2 ops (bit-*, HMR) were already gated; ;; Tier-1 quantum ops were not until sweep-041 surfaced the gap ;; via verify-boundary-cond-replay round-trip failure. Clifford ;; counter also gated to mirror sim_cpu.c line 38-39. (when (= (st-cond s) 1) (qref-flip! s (car (cdr op))) (set-clifford! s (+ (st-clifford s) 1)))) ((z) ;; Z is phase-only — no effect on classical-bit single-shot state. ;; Clifford counter gated on cond like other Cliffords. (when (= (st-cond s) 1) (set-clifford! s (+ (st-clifford s) 1)))) ((cx) (when (= (st-cond s) 1) (let ((ctrl (car (cdr op))) (tgt (car (cdr (cdr op))))) (when (= (qref->bit s ctrl) 1) (qref-flip! s tgt))) (set-clifford! s (+ (st-clifford s) 1)))) ((cz) ;; CZ is phase-only — no bit-value effect. Gated counter bump. (when (= (st-cond s) 1) (set-clifford! s (+ (st-clifford s) 1)))) ((ccx) (when (= (st-cond s) 1) (let ((c1 (car (cdr op))) (c2 (car (cdr (cdr op)))) (tgt (car (cdr (cdr (cdr op)))))) (when (and (= (qref->bit s c1) 1) (= (qref->bit s c2) 1)) (qref-flip! s tgt))) ;; sweep-041: bump Toffoli only on cond=1 — mirrors HEAD's ;; sim_cpu.c line 37 (toffoli_gates += executed = POPCOUNT(cond)). ;; Single-shot classical: cond=1 → +1, cond=0 → +0. Without this ;; gating, cond-replay's "half-shot saving" doesn't show in our ;; sim's Toffoli count. (set-toffoli! s (+ (st-toffoli s) 1)))) ((ccz) ;; CCZ is phase-only — no bit-value effect. One Toffoli charge, ;; gated on cond like CCX (matches sim_cpu.c line 36-37). (when (= (st-cond s) 1) (set-toffoli! s (+ (st-toffoli s) 1)))) ((r) ;; Single-qubit reset. Under push-cond=0 the reset must NOT fire ;; (matches HEAD sim_cpu.c line 86-90: qubits &= ~cond). Clifford ;; counter also gated. (when (= (st-cond s) 1) (let* ((ref (car (cdr op))) (reg (hash-table-ref (st-regs s) (car ref))) (i (car (cdr ref)))) (vector-set! reg i 0)) (set-clifford! s (+ (st-clifford s) 1)))) ((bit-invert) (when (= (st-cond s) 1) (let ((id (car (cdr op)))) (bit-set! s id (- 1 (bit-ref s id)))) (set-clifford! s (+ (st-clifford s) 1)))) ((bit-store0) (when (= (st-cond s) 1) (bit-set! s (car (cdr op)) 0) (set-clifford! s (+ (st-clifford s) 1)))) ((bit-store1) (when (= (st-cond s) 1) (bit-set! s (car (cdr op)) 1) (set-clifford! s (+ (st-clifford s) 1)))) ((hmr) ;; Hadamard + Measure + Reset. Forward: bit := qubit, qubit := 0. ;; Reverse: qubit := bit, bit := 0. Gated on conditional. Clifford ;; counter also gated for consistency with sim_cpu.c line 38-39. (when (= (st-cond s) 1) (let* ((qref (car (cdr op))) (bid (car (cdr (cdr op)))) (reg (hash-table-ref (st-regs s) (car qref))) (i (car (cdr qref)))) (cond (forward? (bit-set! s bid (vector-ref reg i)) (vector-set! reg i 0)) (else (vector-set! reg i (bit-ref s bid)) (bit-set! s bid 0)))) (set-clifford! s (+ (st-clifford s) 1)))) ((push-cond) ;; Forward: stack.push(cond); cond := cond AND bit. ;; Reverse: cond := stack.pop(). (cond (forward? (set-cond-stack! s (cons (st-cond s) (st-cond-stack s))) (let ((bid (car (cdr op)))) (set-cond! s (* (st-cond s) (bit-ref s bid))))) (else (cond ((null? (st-cond-stack s)) (set-status! s 'pop-empty-stack)) (else (set-cond! s (car (st-cond-stack s))) (set-cond-stack! s (cdr (st-cond-stack s)))))))) ((pop-cond) ;; Forward: cond := stack.pop(). ;; Reverse: stack.push(cond); cond := cond AND ??? — pop-cond ;; carries no bit-id, so reverse cannot reconstruct the narrowing. ;; Pop-cond's reverse therefore just pushes current cond. ;; (This is reversible iff push-cond and pop-cond bracket cleanly.) (cond (forward? (cond ((null? (st-cond-stack s)) (set-status! s 'pop-empty-stack)) (else (set-cond! s (car (st-cond-stack s))) (set-cond-stack! s (cdr (st-cond-stack s)))))) (else (set-cond-stack! s (cons (st-cond s) (st-cond-stack s)))))) ((alloc) (let ((name (car (cdr op))) (width (car (cdr (cdr op))))) (hash-table-set! (st-widths s) name width) (if forward? (begin (hash-table-set! (st-regs s) name (make-vector width 0)) (set-live! s (+ (st-live s) width)) (touch-peak! s)) (let ((reg (hash-table-ref (st-regs s) name))) (if (vector-all-zero? reg) (begin (hash-table-delete! (st-regs s) name) (set-live! s (- (st-live s) width))) (set-status! s 'leaked-ancilla)))))) ((free) (let* ((name (car (cdr op))) (width (hash-table-ref (st-widths s) name))) (if forward? (let ((reg (hash-table-ref (st-regs s) name))) (if (vector-all-zero? reg) (begin (hash-table-delete! (st-regs s) name) (set-live! s (- (st-live s) width))) (set-status! s 'leaked-ancilla))) (begin (hash-table-set! (st-regs s) name (make-vector width 0)) (set-live! s (+ (st-live s) width)) (touch-peak! s))))) (else (set-status! s 'bad-op)))) (define (run-ops! s ops forward?) "Walk ops in order, halting on first state-status flip." (let loop ((rest ops)) (cond ((null? rest) #t) ((not (st-ok? s)) #f) (else (exec-op! s (car rest) forward?) (loop (cdr rest)))))) ;;; ── simulate one circuit, return result S-expression ────────── (define (simulate circuit) (let* ((regs-sect (section circuit 'registers)) (input-sect (section circuit 'input)) (ops-sect (section circuit 'ops)) (expected (section circuit 'expected-output)) (s (make-sim-state))) (declare-registers! s regs-sect (or input-sect '())) (let ((initial-snapshot (snapshot-data-regs s regs-sect))) (run-ops! s ops-sect #t) (if (not (st-ok? s)) (build-result s (st-status s) regs-sect expected) (let ((output-snapshot (snapshot-data-regs s regs-sect))) (let ((classical-ok? (or (null? expected) (expected-matches? expected output-snapshot)))) (set-toffoli! s 0) (set-clifford! s 0) (run-ops! s (reverse ops-sect) #f) (let ((restored? (and (st-ok? s) (snapshot-equals? s initial-snapshot)))) (cond ((not classical-ok?) (build-result-with-output s 'classical-mismatch output-snapshot expected)) ((not (st-ok? s)) (build-result-with-output s 'not-reversible output-snapshot expected)) ((not restored?) (build-result-with-output s 'not-reversible output-snapshot expected)) (else (build-result-with-output s 'ok output-snapshot expected)))))))))) (define (snapshot-data-regs s regs-sect) "Return alist (name . bit-vector) for declared data registers only." (map (lambda (rec) (let ((name (car rec))) (cons name (vector-copy (hash-table-ref (st-regs s) name))))) regs-sect)) (define (snapshot-equals? s snap) "Check every register in snap matches current state." (let loop ((rest snap)) (cond ((null? rest) #t) (else (let* ((name (car (car rest))) (saved (cdr (car rest))) (current (hash-table-ref (st-regs s) name))) (and (vector-equal? saved current) (loop (cdr rest)))))))) (define (expected-matches? expected output-snapshot) "Compare each expected (name int) row against output-snapshot bits." (let loop ((rest expected)) (cond ((null? rest) #t) (else (let* ((name (car (car rest))) (want (car (cdr (car rest)))) (got-vec (cdr (assoc name output-snapshot))) (got (bits->int got-vec))) (and (= want got) (loop (cdr rest)))))))) (define (build-result s status regs-sect expected) (build-result-with-output s status (snapshot-data-regs s regs-sect) expected)) (define (build-result-with-output s status snap expected) (let ((output-rows (map (lambda (rec) (list (car rec) (bits->int (cdr rec)))) snap)) (toffoli (st-toffoli s)) (peak (st-peak s))) (list 'result (list 'status status) (cons 'output output-rows) (list 'counts (list 'toffoli toffoli) (list 'clifford (st-clifford s)) (list 'peak-qubits peak)) (list 'score (* toffoli peak))))) ;;; ── top-level entrypoint ────────────────────────────────────── (define (simulate-portal in-path out-path) "Read circuit portal, simulate, write result portal." (let* ((circuit (read-portal in-path)) (result (simulate circuit))) (write-portal! out-path result) result))