diff --git a/c/builtins.c b/c/builtins.c index a702a46..f35f84c 100644 --- a/c/builtins.c +++ b/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); diff --git a/c/test.c b/c/test.c index 58d993a..8287358 100644 --- a/c/test.c +++ b/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);