test(embed): pin two-record-param per-tick (State, Tick) -> State on the shipped M3 ABI

Coverage backfill for an already-shipped capability — NO language /
checker / codegen / schema / runtime change (4 new files, all under
examples/ and crates/ail/tests/).

Property protected: the M3 export gate is_c_abi_type runs as a
per-parameter loop, accepting a single-ctor all-scalar record
independently per param; the staticlib forwarder maps every
non-scalar Type::Con to a bare ptr (M3-frozen). So a kernel
(State, Tick) -> State with BOTH params single-ctor all-scalar
records is callable from C today — a record Tick pushed per call,
not just a scalar sample. Every shipped M3 fixture pushes a scalar
Float; none pinned the two-record-param shape (the actual minimal
data-server binding). This is the residual of the retired M4.

Added:
- examples/embed_backtest_step_tick.ail            (own Tick)
- examples/embed_backtest_step_tick_borrow.ail     (borrow Tick)
- crates/ail/tests/embed/tick_roundtrip.c          (C host, -DBORROW)
- crates/ail/tests/embed_tick_e2e.rs               (2 E2E tests)

Both E2E tests pass on HEAD (Boss-verified, not just agent-claimed):
own + borrow, globally leak-free (Sigma allocs == Sigma frees across
the ctx readback + g_rc atexit lines, the M3 dual-stat-line model),
acc/n value-correct, exit 0. Independently re-confirms the M4
retirement reasoning: the capability is genuinely present in M3.

Honest, deliberately NOT silenced: the new fixtures emit an
advisory [over-strict-mode] warning (ail check still exits 0,
non-gating) that the M3 scalar-sample original does not. Root cause
characterized: a conservative over-strict-mode false-positive whose
discriminator is the nested second (match tick ...) — structurally
identical `st` handling warns when its term-ctor rebuild is nested
inside an inner match. It is advisory only and does not affect
codegen, the ABI, or the proven property. The fixture is kept in
its LLM-natural form (dont-adapt-tests-to-bugs); the lint precision
wart is surfaced as a separate finding, not papered over.
This commit is contained in:
2026-05-18 23:28:52 +02:00
parent 38150ae923
commit 170464fca8
4 changed files with 275 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
/* Embedding-ABI M3 two-record-param round-trip C host (own + borrow
* via a compile-time -DBORROW switch). Backfill: M3 shipped fixtures
* only ever push a *scalar* per-tick sample; this host pushes a
* single-ctor all-scalar *record* `Tick` as the per-call payload —
* the actual minimal data-server binding shape. Frozen value layout
* (DESIGN.md §"Embedding ABI" > "Frozen value layout"):
*
* State (24-byte payload):
* p - 8 .. p uint64_t refcount header (set to 1 by ailang_rc_alloc)
* p + 0 int64_t constructor tag (single ctor -> 0)
* p + 8 IEEE-754 double field 0 = Float acc
* p + 16 int64_t field 1 = Int n
* Tick (8-byte payload):
* p - 8 .. p uint64_t refcount header (set to 1 by ailang_rc_alloc)
* p + 0 int64_t constructor tag (single ctor -> 0)
* p + 8 IEEE-754 double field 0 = Float px
*
* own : the kernel consumes BOTH `(own (con State))` and
* `(own (con Tick))` inputs (drop at return); the host must NOT
* touch/dec either after the call.
* borrow: `State` is `(own ...)` (kernel-consumed, as own); `Tick` is
* `(borrow (con Tick))` — the kernel does NOT consume it; the
* host retains the Tick and dec's it itself each iter.
* In both modes the return is host-owned and host-freed.
*/
#include <stdint.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
typedef struct ailang_ctx ailang_ctx_t;
extern ailang_ctx_t *ailang_ctx_new(void);
extern void ailang_ctx_free(ailang_ctx_t *);
extern void *ailang_rc_alloc(size_t);
extern void ailang_rc_dec(void *);
extern void *backtest_step_tick(ailang_ctx_t *ctx, void *st, void *tick);
static void *make_state(double acc, int64_t n) {
void *p = ailang_rc_alloc(8 + 2 * 8); /* header=1 set by runtime */
*(int64_t *)((char *)p + 0) = 0; /* single-ctor tag = 0 */
memcpy((char *)p + 8, &acc, 8); /* Float acc @ 8 */
*(int64_t *)((char *)p + 16) = n; /* Int n @ 16 */
return p;
}
static void *make_tick(double px) {
void *p = ailang_rc_alloc(8 + 1 * 8); /* header=1 set by runtime */
*(int64_t *)((char *)p + 0) = 0; /* single-ctor tag = 0 */
memcpy((char *)p + 8, &px, 8); /* Float px @ 8 */
return p;
}
int main(void) {
ailang_ctx_t *ctx = ailang_ctx_new();
void *st = make_state(0.0, 0);
double expected = 0.0;
for (int i = 0; i < 1000000; i++) {
double px = (double)(i & 7);
expected += px;
void *tick = make_tick(px);
void *next = backtest_step_tick(ctx, st, tick);
#ifdef BORROW
ailang_rc_dec(tick); /* borrow: host retained Tick, frees it */
#endif
/* own: kernel consumed `st` AND `tick`; do NOT dec them here. */
st = next; /* return is host-owned */
}
double acc; memcpy(&acc, (char *)st + 8, 8);
int64_t n = *(int64_t *)((char *)st + 16);
assert(n == 1000000);
assert(acc == expected);
ailang_rc_dec(st); /* host frees the final return */
ailang_ctx_free(ctx); /* AILANG_RC_STATS readback fires here */
return 0;
}