c-tier: emit-circuit-to-ops-bin-stream — streaming-emit primitive
Ports lumbda.py _emit_circuit_to_ops_bin_stream to C tier. Walks a
Scheme registers + ops list, writes each 56-byte QECCOPS1 op record
straight to disk via fopen/fwrite, then seeks back to patch the n_ops
header at the tail. O(1) host memory regardless of n_ops.
Cross-tier byte-identity verified:
* 500-op mixed-tag synthetic circuit on host: Python ↔ C identical
* real-point-add n+1=5 (p=11, 94,214 ops, 5.0 MB) inside VM
* real-point-add n+1=9 (p=251, 674,872 ops, 36 MB) inside VM
sha256 matches every width.
Speedup on equal Scheme source (build + emit combined):
* n+1=5: Python 56.8 s → C 1.1 s (52×)
* n+1=9: Python 365 s → C 8.4 s (43×)
Combined ratio exceeds the prior 11-16× C-tier envelope because the
build phase (Phase B mod-arith construction) also accelerates on C;
emit-only ratio is ~3-9× and grows with op count.
C test suite: 85 → 88 passing (emit-stream-basic, alloc-free, empty).
Shared functional suite: 205/205 still passing on both tiers.
Implementation notes:
* pack_op_record packs u32 kind + u32 pad + 6× u64 LE, matching
op-specs->bytes byte layout exactly.
* Layout hashtable Symbol → fixnum base, mirroring walk-circuit-ops.
* NO_SLOT sentinel: 0xFFFFFFFFFFFFFFFF written directly to u64 slots
that the Scheme side did not populate.
* libc's default fwrite buffer (~4 KB) handles batching at ~73 ops
per write — same throughput class as Python tier's 8 KiB list batch.
This commit is contained in:
parent
661f9a01ec
commit
f398902cc4
2 changed files with 474 additions and 0 deletions
253
c/builtins.c
253
c/builtins.c
|
|
@ -1669,6 +1669,258 @@ static Value bi_count_lumbda_ops(Value *a, int n, Env *e) {
|
|||
return make_vector_from(items, 3);
|
||||
}
|
||||
|
||||
/* emit-circuit-to-ops-bin-stream — streaming-emit walker that writes
|
||||
* each 56-byte QECCOPS1 op record directly to a file as it is produced.
|
||||
* O(1) host memory regardless of n_ops. C-tier port of lumbda.py
|
||||
* _emit_circuit_to_ops_bin_stream. Byte-identical output across tiers.
|
||||
*
|
||||
* Calling shape (matches Python tier):
|
||||
* (emit-circuit-to-ops-bin-stream out-path registers ops)
|
||||
* out-path string — destination file
|
||||
* registers list of (name width) pairs (declared registers)
|
||||
* ops list of (tag …) op records (lumbda ops, same shape as
|
||||
* walk-circuit-ops input)
|
||||
* → returns n_ops written (fixnum)
|
||||
*
|
||||
* File format (QECCOPS1):
|
||||
* magic 8B "QECCOPS1"
|
||||
* n_ops 8B u64 LE (placeholder zero, patched at tail via fseek)
|
||||
* body n_ops × 56B (matches op-specs->bytes layout exactly)
|
||||
*
|
||||
* Per-op record layout (56 B little-endian):
|
||||
* u32 kind | u32 pad=0 | u64 q2 | u64 q1 | u64 qt | u64 ct | u64 cc | u64 rt
|
||||
*
|
||||
* Kind dispatch & layout mirror bi_walk_circuit_ops (above) — single
|
||||
* source of truth. NO_SLOT representation: 0xFFFFFFFFFFFFFFFF, written
|
||||
* directly into u64 slots that the Scheme side did not populate.
|
||||
*
|
||||
* Buffering: libc's default fwrite buffer (~4 KB) plus the 56-byte
|
||||
* records means each disk write covers ~73 ops — same throughput class
|
||||
* as the Python tier's 8 KiB manual batch. fflush + fclose at the tail
|
||||
* commit everything before the function returns.
|
||||
*
|
||||
* Layout map: ULHashTable Symbol → fixnum base (same as walk-circuit-ops).
|
||||
* Returning the symbol's qubit base; we never need width after the
|
||||
* Register/Append boilerplate is emitted, so we store just the base. */
|
||||
static void pack_op_record(char *buf, uint32_t kind,
|
||||
uint64_t q2, uint64_t q1, uint64_t qt,
|
||||
uint64_t ct, uint64_t cc, uint64_t rt) {
|
||||
/* u32 kind */
|
||||
buf[0] = (char)(kind & 0xFF);
|
||||
buf[1] = (char)((kind >> 8) & 0xFF);
|
||||
buf[2] = (char)((kind >> 16) & 0xFF);
|
||||
buf[3] = (char)((kind >> 24) & 0xFF);
|
||||
/* u32 pad */
|
||||
buf[4] = 0; buf[5] = 0; buf[6] = 0; buf[7] = 0;
|
||||
/* 6× u64 LE */
|
||||
uint64_t u[6] = { q2, q1, qt, ct, cc, rt };
|
||||
for (int i = 0; i < 6; i++) {
|
||||
size_t off = 8 + i * 8;
|
||||
uint64_t x = u[i];
|
||||
buf[off + 0] = (char)(x & 0xFF);
|
||||
buf[off + 1] = (char)((x >> 8) & 0xFF);
|
||||
buf[off + 2] = (char)((x >> 16) & 0xFF);
|
||||
buf[off + 3] = (char)((x >> 24) & 0xFF);
|
||||
buf[off + 4] = (char)((x >> 32) & 0xFF);
|
||||
buf[off + 5] = (char)((x >> 40) & 0xFF);
|
||||
buf[off + 6] = (char)((x >> 48) & 0xFF);
|
||||
buf[off + 7] = (char)((x >> 56) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
/* Write one op record to f, bumping count. Returns 0 on success, -1 on
|
||||
* fwrite failure (caller closes & errors). */
|
||||
#define NO_SLOT_U64 0xFFFFFFFFFFFFFFFFULL
|
||||
static int emit_one(FILE *f, uint64_t *count,
|
||||
uint32_t kind,
|
||||
uint64_t q2, uint64_t q1, uint64_t qt,
|
||||
uint64_t ct, uint64_t cc, uint64_t rt) {
|
||||
char rec[56];
|
||||
pack_op_record(rec, kind, q2, q1, qt, ct, cc, rt);
|
||||
if (fwrite(rec, 1, 56, f) != 56) return -1;
|
||||
(*count)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Emit Register + width × AppendToRegister boilerplate.
|
||||
* Reg-id = base = next_q at time of emit (matches Python tier semantics:
|
||||
* reg_id = base in _emit_circuit_to_ops_bin_stream::emit_register).
|
||||
* Updates layout (name → base) and returns new next_q.
|
||||
* Returns -1 on fwrite failure, 0 on success. */
|
||||
static int emit_register_stream(FILE *f, uint64_t *count,
|
||||
ULHashTable *layout,
|
||||
Value name, int64_t width,
|
||||
int64_t *next_q) {
|
||||
int64_t base = *next_q;
|
||||
uint64_t reg_id = (uint64_t)base;
|
||||
ht_set(layout, name, VAL_INT(base));
|
||||
/* Register record. */
|
||||
if (emit_one(f, count, 1,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64,
|
||||
NO_SLOT_U64, NO_SLOT_U64, reg_id) < 0) return -1;
|
||||
/* width × AppendToRegister. */
|
||||
for (int64_t i = 0; i < width; i++) {
|
||||
if (emit_one(f, count, 2,
|
||||
NO_SLOT_U64, NO_SLOT_U64,
|
||||
(uint64_t)(base + i),
|
||||
NO_SLOT_U64, NO_SLOT_U64, reg_id) < 0) return -1;
|
||||
}
|
||||
*next_q += width;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Value bi_emit_circuit_to_ops_bin_stream(Value *a, int n, Env *e) {
|
||||
(void)e; CHECK_ARITY("emit-circuit-to-ops-bin-stream", 3);
|
||||
check_string(a[0]);
|
||||
walk_syms_init();
|
||||
|
||||
const char *out_path = AS_STRING(a[0])->data;
|
||||
Value registers_lst = a[1];
|
||||
Value ops_lst = a[2];
|
||||
|
||||
FILE *f = fopen(out_path, "wb");
|
||||
if (!f) lisp_error("emit-circuit-to-ops-bin-stream: cannot open %s", out_path);
|
||||
|
||||
/* Header: magic + zero placeholder for n_ops (patched at tail). */
|
||||
if (fwrite("QECCOPS1", 1, 8, f) != 8) {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: write magic failed");
|
||||
}
|
||||
char zero8[8] = {0,0,0,0,0,0,0,0};
|
||||
if (fwrite(zero8, 1, 8, f) != 8) {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: write header n_ops placeholder failed");
|
||||
}
|
||||
|
||||
Value layout_v = make_hashtable();
|
||||
ULHashTable *layout = AS_HASHTABLE(layout_v);
|
||||
int64_t next_q = 0;
|
||||
uint64_t count = 0;
|
||||
int err = 0;
|
||||
|
||||
/* Declared registers first (boilerplate before ops). */
|
||||
while (IS_PAIR(registers_lst)) {
|
||||
Value rec = CAR(registers_lst);
|
||||
Value name = CAR(rec);
|
||||
int64_t width = as_int(CADR(rec));
|
||||
if (emit_register_stream(f, &count, layout, name, width, &next_q) < 0) {
|
||||
err = 1; break;
|
||||
}
|
||||
registers_lst = CDR(registers_lst);
|
||||
}
|
||||
|
||||
/* Walk ops. */
|
||||
while (!err && IS_PAIR(ops_lst)) {
|
||||
Value op = CAR(ops_lst);
|
||||
Value tag = CAR(op);
|
||||
Value rest = CDR(op);
|
||||
|
||||
if (tag == g_sym_ccx) {
|
||||
Value c1 = CAR(rest);
|
||||
Value c2 = CAR(CDR(rest));
|
||||
Value tgt = CAR(CDR(CDR(rest)));
|
||||
int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
|
||||
int64_t q2 = layout_base(layout, CAR(c2)) + as_int(CADR(c2));
|
||||
int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
|
||||
if (emit_one(f, &count, 13,
|
||||
(uint64_t)q2, (uint64_t)q1, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else if (tag == g_sym_cx) {
|
||||
Value c1 = CAR(rest);
|
||||
Value tgt = CAR(CDR(rest));
|
||||
int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
|
||||
int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
|
||||
if (emit_one(f, &count, 8,
|
||||
NO_SLOT_U64, (uint64_t)q1, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else if (tag == g_sym_x) {
|
||||
Value tgt = CAR(rest);
|
||||
int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
|
||||
if (emit_one(f, &count, 6,
|
||||
NO_SLOT_U64, NO_SLOT_U64, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else if (tag == g_sym_alloc) {
|
||||
Value name = CAR(rest);
|
||||
int64_t width = as_int(CADR(rest));
|
||||
if (emit_register_stream(f, &count, layout, name, width, &next_q) < 0) err = 1;
|
||||
} else if (tag == g_sym_free) {
|
||||
Value name = CAR(rest);
|
||||
ht_delete(layout, name);
|
||||
} else if (tag == g_sym_z) {
|
||||
Value tgt = CAR(rest);
|
||||
int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
|
||||
if (emit_one(f, &count, 7,
|
||||
NO_SLOT_U64, NO_SLOT_U64, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else if (tag == g_sym_cz) {
|
||||
Value c1 = CAR(rest);
|
||||
Value tgt = CAR(CDR(rest));
|
||||
int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
|
||||
int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
|
||||
if (emit_one(f, &count, 9,
|
||||
NO_SLOT_U64, (uint64_t)q1, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else if (tag == g_sym_swap) {
|
||||
Value aa = CAR(rest);
|
||||
Value bb = CAR(CDR(rest));
|
||||
int64_t q1 = layout_base(layout, CAR(aa)) + as_int(CADR(aa));
|
||||
int64_t qt = layout_base(layout, CAR(bb)) + as_int(CADR(bb));
|
||||
if (emit_one(f, &count, 10,
|
||||
NO_SLOT_U64, (uint64_t)q1, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else if (tag == g_sym_ccz) {
|
||||
Value c1 = CAR(rest);
|
||||
Value c2 = CAR(CDR(rest));
|
||||
Value tgt = CAR(CDR(CDR(rest)));
|
||||
int64_t q1 = layout_base(layout, CAR(c1)) + as_int(CADR(c1));
|
||||
int64_t q2 = layout_base(layout, CAR(c2)) + as_int(CADR(c2));
|
||||
int64_t qt = layout_base(layout, CAR(tgt)) + as_int(CADR(tgt));
|
||||
if (emit_one(f, &count, 14,
|
||||
(uint64_t)q2, (uint64_t)q1, (uint64_t)qt,
|
||||
NO_SLOT_U64, NO_SLOT_U64, NO_SLOT_U64) < 0) err = 1;
|
||||
} else {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: unknown op tag");
|
||||
}
|
||||
ops_lst = CDR(ops_lst);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: fwrite failed");
|
||||
}
|
||||
|
||||
/* Patch n_ops header. u64 LE at offset 8. */
|
||||
if (fflush(f) != 0) {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: fflush failed");
|
||||
}
|
||||
if (fseek(f, 8, SEEK_SET) != 0) {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: fseek failed");
|
||||
}
|
||||
char nbuf[8];
|
||||
uint64_t nu = count;
|
||||
nbuf[0] = (char)(nu & 0xFF);
|
||||
nbuf[1] = (char)((nu >> 8) & 0xFF);
|
||||
nbuf[2] = (char)((nu >> 16) & 0xFF);
|
||||
nbuf[3] = (char)((nu >> 24) & 0xFF);
|
||||
nbuf[4] = (char)((nu >> 32) & 0xFF);
|
||||
nbuf[5] = (char)((nu >> 40) & 0xFF);
|
||||
nbuf[6] = (char)((nu >> 48) & 0xFF);
|
||||
nbuf[7] = (char)((nu >> 56) & 0xFF);
|
||||
if (fwrite(nbuf, 1, 8, f) != 8) {
|
||||
fclose(f);
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: header patch write failed");
|
||||
}
|
||||
if (fclose(f) != 0) {
|
||||
lisp_error("emit-circuit-to-ops-bin-stream: fclose failed");
|
||||
}
|
||||
|
||||
return VAL_INT((int64_t)count);
|
||||
}
|
||||
#undef NO_SLOT_U64
|
||||
|
||||
/* heap-snapshot / heap-restore: asm-only arena primitives. The asm impl
|
||||
* has no GC; these let a server rewind its bump allocator between
|
||||
* requests. Python + C have real GCs — no-ops here so portable .lsp
|
||||
|
|
@ -2359,6 +2611,7 @@ Env *make_global_env(void) {
|
|||
DEF("walk-circuit-ops", bi_walk_circuit_ops);
|
||||
DEF("op-specs->bytes", bi_op_specs_to_bytes);
|
||||
DEF("count-lumbda-ops", bi_count_lumbda_ops);
|
||||
DEF("emit-circuit-to-ops-bin-stream", bi_emit_circuit_to_ops_bin_stream);
|
||||
DEF("heap-snapshot", bi_heap_snapshot);
|
||||
DEF("heap-restore", bi_heap_restore);
|
||||
DEF("current-time-ms", bi_current_time_ms);
|
||||
|
|
|
|||
221
c/test.c
221
c/test.c
|
|
@ -914,6 +914,222 @@ TEST(portal_checkpoint_builtin) {
|
|||
unlink("/tmp/test_checkpoint.json");
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* emit-circuit-to-ops-bin-stream — streaming-emit primitive
|
||||
*
|
||||
* Tests build a small register + ops list via the Scheme reader, call the
|
||||
* primitive against a temp file, then read the bytes back & compare byte-
|
||||
* for-byte against a hand-rolled QECCOPS1 layout. Catches both kind/slot
|
||||
* packing & header-patch path. Cross-tier byte-identity with Python lives
|
||||
* outside the C unit-test harness (foxhop.net's vm-runner.sh envelope).
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Hand-roll a 56-byte op record matching pack_op_record / op-specs->bytes. */
|
||||
static void pack_rec_expected(unsigned char *buf, uint32_t kind,
|
||||
uint64_t q2, uint64_t q1, uint64_t qt,
|
||||
uint64_t ct, uint64_t cc, uint64_t rt) {
|
||||
buf[0] = (unsigned char)(kind & 0xFF);
|
||||
buf[1] = (unsigned char)((kind >> 8) & 0xFF);
|
||||
buf[2] = (unsigned char)((kind >> 16) & 0xFF);
|
||||
buf[3] = (unsigned char)((kind >> 24) & 0xFF);
|
||||
buf[4] = 0; buf[5] = 0; buf[6] = 0; buf[7] = 0;
|
||||
uint64_t slots[6] = { q2, q1, qt, ct, cc, rt };
|
||||
for (int i = 0; i < 6; i++) {
|
||||
size_t off = 8 + i * 8;
|
||||
uint64_t x = slots[i];
|
||||
buf[off + 0] = (unsigned char)(x & 0xFF);
|
||||
buf[off + 1] = (unsigned char)((x >> 8) & 0xFF);
|
||||
buf[off + 2] = (unsigned char)((x >> 16) & 0xFF);
|
||||
buf[off + 3] = (unsigned char)((x >> 24) & 0xFF);
|
||||
buf[off + 4] = (unsigned char)((x >> 32) & 0xFF);
|
||||
buf[off + 5] = (unsigned char)((x >> 40) & 0xFF);
|
||||
buf[off + 6] = (unsigned char)((x >> 48) & 0xFF);
|
||||
buf[off + 7] = (unsigned char)((x >> 56) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
#define NO_SLOT_TEST 0xFFFFFFFFFFFFFFFFULL
|
||||
|
||||
TEST(emit_stream_basic) {
|
||||
/* Small circuit: one declared register 'a' width 3, then x/cx/ccx ops.
|
||||
* Run primitive into temp file. Compare bytes against expected layout.
|
||||
*
|
||||
* Expected ops sequence:
|
||||
* Register reg-id=0
|
||||
* Append qt=0 reg=0
|
||||
* Append qt=1 reg=0
|
||||
* Append qt=2 reg=0
|
||||
* X qt=1
|
||||
* CX q1=0 qt=2
|
||||
* CCX q2=2 q1=0 qt=1
|
||||
* Total: 7 ops. */
|
||||
const char *path = "/tmp/test_emit_stream_basic.bin";
|
||||
unlink(path);
|
||||
char src[1024];
|
||||
snprintf(src, sizeof(src),
|
||||
"(emit-circuit-to-ops-bin-stream \"%s\" '((a 3)) "
|
||||
" '((x (a 1)) (cx (a 0) (a 2)) (ccx (a 0) (a 2) (a 1))))",
|
||||
path);
|
||||
Value r = run(src);
|
||||
ASSERT(IS_INT(r), "primitive must return fixnum");
|
||||
/* 1 Register + 3 Append + 1 X + 1 CX + 1 CCX = 7. */
|
||||
ASSERT_EQ_INT(as_int(r), 7);
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
ASSERT(f != NULL, "output file must exist");
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
/* 16-byte header + 7×56 = 408. */
|
||||
ASSERT_EQ_INT(sz, 16 + 7 * 56);
|
||||
unsigned char *got = (unsigned char *)ul_malloc(sz);
|
||||
ASSERT(fread(got, 1, sz, f) == (size_t)sz, "read full file");
|
||||
fclose(f);
|
||||
|
||||
/* Build expected bytes. */
|
||||
unsigned char exp[16 + 7 * 56];
|
||||
memcpy(exp, "QECCOPS1", 8);
|
||||
/* n_ops = 7 LE. */
|
||||
exp[8] = 7; exp[9] = 0; exp[10] = 0; exp[11] = 0;
|
||||
exp[12] = 0; exp[13] = 0; exp[14] = 0; exp[15] = 0;
|
||||
size_t off = 16;
|
||||
/* kind 1 Register, reg-id=0. */
|
||||
pack_rec_expected(exp + off, 1,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, NO_SLOT_TEST,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, 0); off += 56;
|
||||
/* 3 × Append. */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
pack_rec_expected(exp + off, 2,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, (uint64_t)i,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, 0); off += 56;
|
||||
}
|
||||
/* X qt=1. */
|
||||
pack_rec_expected(exp + off, 6,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, 1,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, NO_SLOT_TEST); off += 56;
|
||||
/* CX q1=0 qt=2. */
|
||||
pack_rec_expected(exp + off, 8,
|
||||
NO_SLOT_TEST, 0, 2,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, NO_SLOT_TEST); off += 56;
|
||||
/* CCX q2=2 q1=0 qt=1. */
|
||||
pack_rec_expected(exp + off, 13,
|
||||
2, 0, 1,
|
||||
NO_SLOT_TEST, NO_SLOT_TEST, NO_SLOT_TEST); off += 56;
|
||||
|
||||
for (long i = 0; i < sz; i++) {
|
||||
if (got[i] != exp[i]) {
|
||||
ul_free(got);
|
||||
unlink(path);
|
||||
lisp_error("byte %ld differs: got 0x%02x, expected 0x%02x",
|
||||
i, got[i], exp[i]);
|
||||
}
|
||||
}
|
||||
ul_free(got);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
TEST(emit_stream_alloc_free) {
|
||||
/* Exercise alloc & free op tags: declared register, then alloc ancilla,
|
||||
* use it, free it, alloc again (reusing next-q monotonic). The C tier
|
||||
* does NOT pool — base monotonically grows. Mirrors Python behavior. */
|
||||
const char *path = "/tmp/test_emit_stream_alloc_free.bin";
|
||||
unlink(path);
|
||||
char src[2048];
|
||||
snprintf(src, sizeof(src),
|
||||
"(emit-circuit-to-ops-bin-stream \"%s\" '((tx 2)) "
|
||||
" '((alloc anc 2)"
|
||||
" (cx (tx 0) (anc 1))"
|
||||
" (free anc)"
|
||||
" (alloc anc2 1)"
|
||||
" (x (anc2 0))))",
|
||||
path);
|
||||
Value r = run(src);
|
||||
ASSERT(IS_INT(r), "primitive must return fixnum");
|
||||
/* tx: Register + 2 Append = 3
|
||||
* alloc anc 2: Register + 2 Append = 3
|
||||
* cx: 1
|
||||
* free: 0 (no op emitted)
|
||||
* alloc anc2 1: Register + 1 Append = 2
|
||||
* x: 1
|
||||
* Total: 10. */
|
||||
ASSERT_EQ_INT(as_int(r), 10);
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
ASSERT(f != NULL, "output file must exist");
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
ASSERT_EQ_INT(sz, 16 + 10 * 56);
|
||||
unsigned char *got = (unsigned char *)ul_malloc(sz);
|
||||
ASSERT(fread(got, 1, sz, f) == (size_t)sz, "read full file");
|
||||
fclose(f);
|
||||
|
||||
/* Verify magic + n_ops header. */
|
||||
ASSERT(memcmp(got, "QECCOPS1", 8) == 0, "magic mismatch");
|
||||
uint64_t n_ops = (uint64_t)got[8]
|
||||
| ((uint64_t)got[9] << 8)
|
||||
| ((uint64_t)got[10] << 16)
|
||||
| ((uint64_t)got[11] << 24)
|
||||
| ((uint64_t)got[12] << 32)
|
||||
| ((uint64_t)got[13] << 40)
|
||||
| ((uint64_t)got[14] << 48)
|
||||
| ((uint64_t)got[15] << 56);
|
||||
ASSERT_EQ_INT((int64_t)n_ops, 10);
|
||||
|
||||
/* Verify cx record: qubit 0 = tx base, qubit 3 = anc base+1.
|
||||
* tx base = 0 (next_q was 0). After tx: next_q = 2.
|
||||
* alloc anc: base = 2. anc 1 = 3. After: next_q = 4.
|
||||
* cx q1 = 0, qt = 3.
|
||||
* Records:
|
||||
* 0-2: tx Register, Append qt=0, Append qt=1 (reg=0)
|
||||
* 3-5: anc Register reg=2, Append qt=2, Append qt=3 (reg=2)
|
||||
* 6: cx q1=0 qt=3
|
||||
* 7-8: anc2 Register reg=4, Append qt=4 (reg=4)
|
||||
* 9: x qt=4
|
||||
*/
|
||||
/* Spot-check the cx record (record index 6, offset = 16 + 6*56 = 352). */
|
||||
size_t cx_off = 16 + 6 * 56;
|
||||
/* kind LE = 8. */
|
||||
ASSERT_EQ_INT(got[cx_off], 8);
|
||||
ASSERT_EQ_INT(got[cx_off + 1], 0);
|
||||
/* q2 slot (offset +8) = NO_SLOT (all 0xFF). */
|
||||
for (int i = 0; i < 8; i++) ASSERT_EQ_INT(got[cx_off + 8 + i], 0xFF);
|
||||
/* q1 slot (offset +16) = 0. */
|
||||
for (int i = 0; i < 8; i++) ASSERT_EQ_INT(got[cx_off + 16 + i], 0);
|
||||
/* qt slot (offset +24) = 3. */
|
||||
ASSERT_EQ_INT(got[cx_off + 24], 3);
|
||||
for (int i = 1; i < 8; i++) ASSERT_EQ_INT(got[cx_off + 24 + i], 0);
|
||||
|
||||
ul_free(got);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
TEST(emit_stream_empty) {
|
||||
/* No registers, no ops — should produce just header w/ n_ops=0. */
|
||||
const char *path = "/tmp/test_emit_stream_empty.bin";
|
||||
unlink(path);
|
||||
char src[256];
|
||||
snprintf(src, sizeof(src),
|
||||
"(emit-circuit-to-ops-bin-stream \"%s\" '() '())", path);
|
||||
Value r = run(src);
|
||||
ASSERT(IS_INT(r), "primitive must return fixnum");
|
||||
ASSERT_EQ_INT(as_int(r), 0);
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
ASSERT(f != NULL, "output file must exist");
|
||||
unsigned char hdr[16];
|
||||
ASSERT(fread(hdr, 1, 16, f) == 16, "read 16-byte header");
|
||||
/* EOF after header. */
|
||||
char extra;
|
||||
ASSERT(fread(&extra, 1, 1, f) == 0, "no body bytes");
|
||||
fclose(f);
|
||||
ASSERT(memcmp(hdr, "QECCOPS1", 8) == 0, "magic mismatch");
|
||||
for (int i = 8; i < 16; i++) ASSERT_EQ_INT(hdr[i], 0);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
#undef NO_SLOT_TEST
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
* Main
|
||||
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||
|
|
@ -1023,6 +1239,11 @@ int main(void) {
|
|||
run_test_portal_save_resume_cont();
|
||||
run_test_portal_checkpoint_builtin();
|
||||
|
||||
printf("\n[emit-stream]\n");
|
||||
run_test_emit_stream_basic();
|
||||
run_test_emit_stream_alloc_free();
|
||||
run_test_emit_stream_empty();
|
||||
|
||||
printf("\n═══════════════════════════════════════════\n");
|
||||
printf("Results: %d/%d passed", tests_passed, tests_run);
|
||||
if (tests_failed > 0) printf(" (%d failed)", tests_failed);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue