/* xce_select.c — select-expression AST with CSE intern table (Phase 4a). * * Phase 3: non-CSE path (level 0) — allocate and fill nodes directly. * Phase 4a: CSE intern table for level > 0 — commutativity sort + hash-cons. */ #define UNIT_TEST 1 /* expose sel_optimize_test_reset in resreq.h */ #include #include #include #include #include #include #include #include "kv.h" #include "mylock.h" #include "resreq.h" #include "utils.h" #include "zarray.h" /* ── optimize level ───────────────────────────────────────────────────── */ static _Atomic int g_sel_optimize_level = -1; /* -1 = unset */ static pthread_t sel_alloc_thread_id; static _Atomic int sel_alloc_thread_registered = 0; /* 1 after sel_register_alloc_thread() */ void sel_optimize_set(int level) { int expected = -1; /* Write-once CAS: only succeeds when still unset (-1). */ atomic_compare_exchange_strong_explicit( &g_sel_optimize_level, &expected, level, memory_order_seq_cst, memory_order_seq_cst); } int sel_get_optimize_level(void) { return atomic_load_explicit(&g_sel_optimize_level, memory_order_acquire); } /* ── CSE intern table ─────────────────────────────────────────────────── */ /* Key for the intern table — uniquely identifies a select node by its * type and operands. Padding bytes are zero-initialised via the struct * literal so that memcmp-based comparison is correct. */ typedef struct { sel_node_type_t type; uintptr_t op1; uintptr_t op2; } sel_node_key_t; /* Build a canonical key, applying commutativity sort for AND/OR/EQ/NE. */ static sel_node_key_t sel_make_key(sel_node_type_t type, uintptr_t op1, uintptr_t op2) { sel_node_key_t key = { .type = type, .op1 = op1, .op2 = op2 }; /* Commutative operators: sort operands so A op B == B op A. */ switch (type) { case SEL_AND: case SEL_OR: case SEL_EQ: case SEL_NE: if (key.op1 > key.op2) { uintptr_t tmp = key.op1; key.op1 = key.op2; key.op2 = tmp; } break; default: break; } /* For RESNAME, res_type (op2) is derived from the resource manager and * res_idx (op1) uniquely identifies the resource — zero op2 in the key. */ if (type == SEL_RESNAME) { key.op2 = 0; } return key; } /* Key operations for the kv table. */ static uint32_t sel_key_hash_fn(void* k) { sel_node_key_t* key = (sel_node_key_t*)k; uint64_t h; h = hash64((uint64_t)key->type); h ^= hash64(key->op1) * UINT64_C(0x9e3779b97f4a7c15); h ^= hash64(key->op2) * UINT64_C(0x6c62272e07bb0142); return (uint32_t)(h ^ (h >> 32)); } static int sel_key_cmp_fn(void* a, void* b) { /* Field-by-field comparison: memcmp on the whole struct would * compare padding bytes between type and op1, which can differ * between the stack-local search key and the heap-allocated copy * stored in the kvtab, causing false misses in key_del. */ sel_node_key_t* ka = (sel_node_key_t*)a; sel_node_key_t* kb = (sel_node_key_t*)b; if (ka->type != kb->type) return 1; if (ka->op1 != kb->op1) return 1; if (ka->op2 != kb->op2) return 1; return 0; } static int sel_key_assign_fn(void** dst, void* src) { sel_node_key_t* copy = (sel_node_key_t*)malloc(sizeof(sel_node_key_t)); if (copy == NULL) { return -ENOMEM; } memcpy(copy, src, sizeof(sel_node_key_t)); *dst = copy; return 1; } static void sel_key_destroy_fn(void* k) { free(k); } static kv_key_ops_t sel_node_key_ops = { .hash = sel_key_hash_fn, .assign = sel_key_assign_fn, .cmp = sel_key_cmp_fn, .destroy = sel_key_destroy_fn, }; /* Hard cap on the intern table size — prevents unbounded memory growth. */ #define SEL_MAX_INTERNED_NODES 1000000 /* Node ID counter — atomically incremented for each new interned node. */ static _Atomic uint32_t sel_node_next_id = 0; /* Global intern table state. */ static struct { mylock_t lock; var table; /* kvtab instance: key=sel_node_key_t*, value=node* */ zarray_t nodes; /* all live interned nodes, for GC sweep */ } sel_node_table; /* ── intern table lifecycle ──────────────────────────────────────────── */ void sel_node_table_init(void) { kv_params_t params = { .nslots = 2003, .keyops = &sel_node_key_ops, .datakiller = NULL, /* GC sweep handles node freeing; kvtab must NOT * free values on fini */ }; mylock_init(&sel_node_table.lock); sel_node_table.table = makeInstance(&kvtab_type, ¶ms); zarray_init(&sel_node_table.nodes, 64, NULL); atomic_store_explicit(&sel_node_next_id, 0, memory_order_seq_cst); } void sel_node_table_fini(void) { if (sel_node_table.table == NULL) { return; } /* Free each interned node before destroying the kvtab. * datakiller=NULL means the kvtab does NOT free node values on destroy. * Iterate caps (not count): zarray_del creates NULL gaps, so live nodes * may reside in slots beyond the count boundary. */ int caps = sel_node_table.nodes.caps; for (int i = 0; i < caps; i++) { xce_select_node_t* node = (xce_select_node_t*)zarray_get(&sel_node_table.nodes, i); if (node == NULL) { continue; } if (node->type == SEL_STR && node->u.strval != NULL) { dereference(node->u.strval); } free(node); } destroy(sel_node_table.table); sel_node_table.table = NULL; zarray_fini(&sel_node_table.nodes); mylock_fini(&sel_node_table.lock); atomic_store_explicit(&sel_node_next_id, 0, memory_order_seq_cst); } /* ── optimize test reset ──────────────────────────────────────────────── */ void sel_optimize_test_reset(void) { sel_node_table_fini(); atomic_store_explicit(&g_sel_optimize_level, -1, memory_order_seq_cst); atomic_store_explicit(&sel_alloc_thread_registered, 0, memory_order_seq_cst); /* sel_alloc_thread_id left as-is; only checked when registered flag is set */ } void sel_register_alloc_thread(void) { sel_alloc_thread_id = pthread_self(); atomic_store_explicit(&sel_alloc_thread_registered, 1, memory_order_release); } /* ── node construction ───────────────────────────────────────────────── */ _Static_assert(sizeof(uintptr_t) == sizeof(double), "SEL_NUM bit-cast requires uintptr_t == sizeof(double) for lossless bit-cast"); /* Fill the union fields of a freshly-allocated node. */ static void fill_select_node(xce_select_node_t* node, sel_node_type_t type, uintptr_t op1, uintptr_t op2) { node->type = type; switch (type) { case SEL_AND: case SEL_OR: case SEL_GT: case SEL_LT: case SEL_GE: case SEL_LE: case SEL_EQ: case SEL_NE: node->u.binop.left = (xce_select_node_t*)op1; node->u.binop.right = (xce_select_node_t*)op2; break; case SEL_NOT: node->u.operand = (xce_select_node_t*)op1; break; case SEL_RESNAME: node->u.resname.res_idx = (int)op1; node->u.resname.res_type = (res_type_t)op2; break; case SEL_NUM: { double v; memcpy(&v, &op1, sizeof(double)); node->u.numval = v; break; } case SEL_STR: node->u.strval = (char*)op1; /* xstring-interned pointer */ break; case SEL_ANY: case SEL_TRUE: case SEL_FALSE: /* Leaf nodes with no operands */ break; } } xce_select_node_t* makeSelectNode(sel_node_type_t type, uintptr_t op1, uintptr_t op2) { int level = sel_get_optimize_level(); if (level < 0) { /* Unconfigured — sel_optimize_set must be called first */ return NULL; } if (level == 0) { /* Non-CSE fast path: allocate, fill, return — no interning. */ xce_select_node_t* node = (xce_select_node_t*)calloc(1, sizeof(*node)); if (node == NULL) { if (type == SEL_STR) dereference((char*)op1); return NULL; } fill_select_node(node, type, op1, op2); return node; } /* ── CSE path (level > 0) ────────────────────────────────────────── */ /* Canonicalise commutative operators so A op B and B op A share one key. */ switch (type) { case SEL_AND: case SEL_OR: case SEL_EQ: case SEL_NE: if (op1 > op2) { uintptr_t tmp = op1; op1 = op2; op2 = tmp; } break; default: break; } sel_node_key_t key = sel_make_key(type, op1, op2); /* Step 1: Fast lookup under lock before allocating. */ mylock_lock(&sel_node_table.lock); xce_select_node_t* existing = NULL; int rc = key_find(sel_node_table.table, &key, (void**)&existing); if (rc > 0) { mylock_unlock(&sel_node_table.lock); if (type == SEL_STR) dereference((char*)op1); /* release caller's ref */ return existing; } if (rc == -EINVAL) { mylock_unlock(&sel_node_table.lock); if (type == SEL_STR) dereference((char*)op1); return NULL; } /* Step 1a: Reject new insertions when the cap is reached. Cache hits * above are still served at cap — only new nodes are blocked. */ if (atomic_load_explicit(&sel_node_next_id, memory_order_relaxed) >= SEL_MAX_INTERNED_NODES) { mylock_unlock(&sel_node_table.lock); if (type == SEL_STR) dereference((char*)op1); errno = ENOMEM; return NULL; } mylock_unlock(&sel_node_table.lock); /* Step 2: Cache miss — allocate and fill BEFORE re-acquiring the lock. * hashcode and u.* must be set here so the TOCTOU rollback path can * call dereference(node->u.strval) safely. */ xce_select_node_t* node = (xce_select_node_t*)calloc(1, sizeof(*node)); if (node == NULL) { if (type == SEL_STR) dereference((char*)op1); errno = ENOMEM; return NULL; } node->hashcode = sel_key_hash_fn(&key); fill_select_node(node, type, op1, op2); /* Step 3: Re-acquire lock for TOCTOU re-check and insertion. */ mylock_lock(&sel_node_table.lock); existing = NULL; rc = key_find(sel_node_table.table, &key, (void**)&existing); if (rc > 0) { mylock_unlock(&sel_node_table.lock); if (node->type == SEL_STR) dereference(node->u.strval); free(node); return existing; } /* Re-check cap under lock — another thread may have filled the table. */ if (atomic_load_explicit(&sel_node_next_id, memory_order_relaxed) >= SEL_MAX_INTERNED_NODES) { mylock_unlock(&sel_node_table.lock); if (node->type == SEL_STR) dereference(node->u.strval); free(node); errno = ENOMEM; return NULL; } /* Step 4: Insert into the intern table. */ int rc2 = key_add(sel_node_table.table, &key, node); if (rc2 < 0) { if (rc2 == -EEXIST) { /* Narrow race after the re-check — recover the winning entry. */ existing = NULL; key_find(sel_node_table.table, &key, (void**)&existing); } mylock_unlock(&sel_node_table.lock); if (node->type == SEL_STR) dereference(node->u.strval); free(node); return existing; /* NULL when not -EEXIST */ } /* Step 5: Assign node_id and register in the GC linear array — both * under lock so staging threads cannot produce duplicate IDs. * node_id is assigned here (after key_add) so TOCTOU losers need no * counter rollback. */ node->node_id = atomic_fetch_add_explicit(&sel_node_next_id, 1, memory_order_release); int zrc = zarray_store(&sel_node_table.nodes, node); if (zrc < 0) { /* zarray OOM — roll back the ID and the kvtab entry. */ atomic_fetch_sub_explicit(&sel_node_next_id, 1, memory_order_release); key_del(sel_node_table.table, &key); mylock_unlock(&sel_node_table.lock); if (node->type == SEL_STR) dereference(node->u.strval); free(node); errno = ENOMEM; return NULL; } mylock_unlock(&sel_node_table.lock); return node; } /* ── node/select tree cleanup ────────────────────────────────────────── */ /* Recursive post-order free for non-CSE mode only. * Must not be called when CSE intern table is active — nodes are owned by * the GC table and must not be individually freed. */ static void free_select_tree(xce_select_node_t* node) { if (sel_get_optimize_level() > 0) { ERROR("free_select_tree called in CSE-enabled mode"); abort(); } if (node == NULL) return; switch (node->type) { case SEL_AND: case SEL_OR: case SEL_GT: case SEL_LT: case SEL_GE: case SEL_LE: case SEL_EQ: case SEL_NE: free_select_tree(node->u.binop.left); free_select_tree(node->u.binop.right); break; case SEL_NOT: free_select_tree(node->u.operand); break; case SEL_STR: dereference(node->u.strval); break; default: break; } free(node); } /* Public wrapper used by kill_primary_resreq in resreq.c. */ void sel_free_tree(xce_select_node_t* node) { free_select_tree(node); } static void kill_select_node(xce_select_node_t* node) { if (node == NULL) return; if (sel_get_optimize_level() > 0) { /* CSE mode: nodes are owned by the intern table; freeing them directly * corrupts GC state. Callers must not use this path in CSE mode. */ abort(); } switch (node->type) { case SEL_AND: case SEL_OR: case SEL_GT: case SEL_LT: case SEL_GE: case SEL_LE: case SEL_EQ: case SEL_NE: kill_select_node(node->u.binop.left); kill_select_node(node->u.binop.right); break; case SEL_NOT: kill_select_node(node->u.operand); break; case SEL_STR: dereference(node->u.strval); break; case SEL_RESNAME: case SEL_NUM: case SEL_ANY: case SEL_TRUE: case SEL_FALSE: break; } free(node); } void kill_select(xce_select_t* sel) { if (sel == NULL) return; kill_select_node(sel->root); free(sel); } /* ── eval context lifecycle ───────────────────────────────────────────── */ select_eval_ctx_t* select_eval_ctx_init(uint32_t max_node_id) { select_eval_ctx_t* ctx = calloc(1, sizeof(*ctx)); if (!ctx) { ERROR("OOM in select_eval_ctx_init"); abort(); } ctx->node_count = max_node_id; ctx->bool_bits_cap = max_node_id; ctx->num_vals_cap = max_node_id; ctx->str_vals_cap = max_node_id; if (max_node_id > 0) { size_t bb_bytes = ((size_t)max_node_id * 2 + 7) / 8; ctx->bool_bits = calloc(bb_bytes, 1); ctx->num_vals = calloc(max_node_id, sizeof(double)); ctx->str_vals = calloc(max_node_id, sizeof(const char*)); if (!ctx->bool_bits || !ctx->num_vals || !ctx->str_vals) { free(ctx->bool_bits); free(ctx->num_vals); free(ctx->str_vals); free(ctx); ERROR("OOM partial alloc in select_eval_ctx_init"); abort(); } } return ctx; } void select_eval_ctx_reset(select_eval_ctx_t* ctx, const host_eval_ctx_t* host) { ctx->host = host; if (ctx->bool_bits && ctx->bool_bits_cap > 0) { size_t bytes = ((size_t)ctx->bool_bits_cap * 2 + 7) / 8; memset(ctx->bool_bits, 0, bytes); } } void select_eval_ctx_fini(select_eval_ctx_t* ctx) { if (!ctx) return; free(ctx->bool_bits); free(ctx->num_vals); free(ctx->str_vals); free(ctx); } uint32_t sel_get_node_count(void) { return atomic_load_explicit(&sel_node_next_id, memory_order_acquire); } /* ── 2-bit bitmap helpers (EVAL_NONE=0 so memset(0) clears correctly) ── */ #define EVAL_NONE 0 #define EVAL_TRUE 1 #define EVAL_FALSE 2 #define EVAL_ERROR 3 _Static_assert(EVAL_NONE == 0, "bool_bits reset via memset(0) requires EVAL_NONE==0"); static inline int eval_get_bool(const select_eval_ctx_t* ctx, uint32_t id) { uint32_t bit_off = id * 2; return (ctx->bool_bits[bit_off / 8] >> (bit_off % 8)) & 0x3; } static inline void eval_set_bool(select_eval_ctx_t* ctx, uint32_t id, int val) { uint32_t bit_off = id * 2; uint32_t byte = bit_off / 8; uint32_t shift = bit_off % 8; ctx->bool_bits[byte] = (ctx->bool_bits[byte] & ~((uint8_t)(0x3 << shift))) | (uint8_t)((val & 0x3) << shift); } /* ── comparison helpers ───────────────────────────────────────────────── */ static int is_str_cmp(const xce_select_node_t* node) { const xce_select_node_t* l = node->u.binop.left; const xce_select_node_t* r = node->u.binop.right; if (l->type == SEL_STR || r->type == SEL_STR) return 1; if (l->type == SEL_RESNAME && l->u.resname.res_type == RES_TYPE_STRING) return 1; if (r->type == SEL_RESNAME && r->u.resname.res_type == RES_TYPE_STRING) return 1; return 0; } static double simple_num(const xce_select_node_t* n, const host_eval_ctx_t* h) { if (n->type == SEL_NUM) return n->u.numval; int idx = n->u.resname.res_idx; if (h->numvals == NULL || idx < 0 || idx >= h->num_res) return 0.0; return h->numvals[idx]; } static const char* simple_str(const xce_select_node_t* n, const host_eval_ctx_t* h) { if (n->type == SEL_STR) return n->u.strval; int idx = n->u.resname.res_idx; if (h->strvals == NULL || idx < 0 || idx >= h->num_res) return NULL; return h->strvals[idx]; } /* Safe cache reads: use cached value when in-bounds, fall back to host data * when node_id >= cap (e.g., ctx initialised before this node was interned). */ static double ctx_num(const select_eval_ctx_t* ctx, const xce_select_node_t* n) { return (n->node_id < ctx->num_vals_cap) ? ctx->num_vals[n->node_id] : simple_num(n, ctx->host); } static const char* ctx_str(const select_eval_ctx_t* ctx, const xce_select_node_t* n) { return (n->node_id < ctx->str_vals_cap) ? ctx->str_vals[n->node_id] : simple_str(n, ctx->host); } /* ── eval_select_simple: tree-walk, no memoization (level == 0) ─────── */ int eval_select_simple(const xce_select_node_t* root, const host_eval_ctx_t* host) { if (root == NULL) return 1; switch (root->type) { case SEL_TRUE: return 1; case SEL_FALSE: return 0; case SEL_ANY: return 1; case SEL_RESNAME: switch (root->u.resname.res_type) { case RES_TYPE_BOOL: case RES_TYPE_NUMERIC: { int idx = root->u.resname.res_idx; if (host->numvals == NULL || idx < 0 || idx >= host->num_res) return 0; return host->numvals[idx] != 0.0 ? 1 : 0; } case RES_TYPE_STRING: return -1; } return -1; case SEL_NUM: return root->u.numval != 0.0 ? 1 : 0; case SEL_STR: return -1; case SEL_NOT: { int v = eval_select_simple(root->u.operand, host); return (v < 0) ? -1 : !v; } case SEL_AND: { int l = eval_select_simple(root->u.binop.left, host); if (l <= 0) return l; return eval_select_simple(root->u.binop.right, host); } case SEL_OR: { int l = eval_select_simple(root->u.binop.left, host); if (l > 0) return 1; if (l < 0) return l; return eval_select_simple(root->u.binop.right, host); } case SEL_GT: return simple_num(root->u.binop.left, host) > simple_num(root->u.binop.right, host) ? 1 : 0; case SEL_LT: return simple_num(root->u.binop.left, host) < simple_num(root->u.binop.right, host) ? 1 : 0; case SEL_GE: return simple_num(root->u.binop.left, host) >= simple_num(root->u.binop.right, host) ? 1 : 0; case SEL_LE: return simple_num(root->u.binop.left, host) <= simple_num(root->u.binop.right, host) ? 1 : 0; case SEL_EQ: if (is_str_cmp(root)) return xce_str_match(simple_str(root->u.binop.left, host), simple_str(root->u.binop.right, host)); return simple_num(root->u.binop.left, host) == simple_num(root->u.binop.right, host) ? 1 : 0; case SEL_NE: if (is_str_cmp(root)) return !xce_str_match(simple_str(root->u.binop.left, host), simple_str(root->u.binop.right, host)) ? 1 : 0; return simple_num(root->u.binop.left, host) != simple_num(root->u.binop.right, host) ? 1 : 0; } ERROR("eval_select_simple: unknown node type %d", (int)root->type); abort(); } /* ── memoized eval (level > 0) ───────────────────────────────────────── */ /* Forward declaration — eval_select and eval_select_inner are mutually * recursive: eval_select checks the bitmap cache then calls eval_select_inner; * eval_select_inner calls eval_select on children so leaf-cache writes * (num_vals, str_vals) happen before the parent reads them. */ static int eval_select_inner(const xce_select_node_t* node, select_eval_ctx_t* ctx); int eval_select(const xce_select_node_t* node, select_eval_ctx_t* ctx) { if (node == NULL) return 1; if (sel_get_optimize_level() <= 0) { ERROR("eval_select called with optimize_level <= 0"); abort(); } if (atomic_load_explicit(&sel_alloc_thread_registered, memory_order_acquire) && !pthread_equal(pthread_self(), sel_alloc_thread_id)) { ERROR("eval_select called from non-allocator thread"); return -1; } /* bitmap cache hit */ if (node->node_id < ctx->bool_bits_cap) { int cached = eval_get_bool(ctx, node->node_id); if (cached == EVAL_TRUE) return 1; if (cached == EVAL_FALSE) return 0; if (cached == EVAL_ERROR) return -1; } int result = eval_select_inner(node, ctx); if (node->node_id < ctx->bool_bits_cap) { int bits = (result > 0) ? EVAL_TRUE : (result == 0) ? EVAL_FALSE : EVAL_ERROR; eval_set_bool(ctx, node->node_id, bits); } return result; } static int eval_select_inner(const xce_select_node_t* node, select_eval_ctx_t* ctx) { if (node == NULL) return 1; const host_eval_ctx_t* host = ctx->host; switch (node->type) { case SEL_TRUE: return 1; case SEL_FALSE: return 0; case SEL_ANY: return 1; case SEL_RESNAME: switch (node->u.resname.res_type) { case RES_TYPE_BOOL: case RES_TYPE_NUMERIC: { int idx = node->u.resname.res_idx; double v = (host->numvals != NULL && idx >= 0 && idx < host->num_res) ? host->numvals[idx] : 0.0; if (node->node_id < ctx->num_vals_cap) ctx->num_vals[node->node_id] = v; return v != 0.0 ? 1 : 0; } case RES_TYPE_STRING: { int idx = node->u.resname.res_idx; const char* sv = (host->strvals != NULL && idx >= 0 && idx < host->num_res) ? host->strvals[idx] : NULL; if (node->node_id < ctx->str_vals_cap) ctx->str_vals[node->node_id] = sv; return -1; } } return -1; case SEL_NUM: { if (node->node_id < ctx->num_vals_cap) ctx->num_vals[node->node_id] = node->u.numval; return node->u.numval != 0.0 ? 1 : 0; } case SEL_STR: { if (node->node_id < ctx->str_vals_cap) ctx->str_vals[node->node_id] = node->u.strval; return -1; } case SEL_NOT: { int v = eval_select(node->u.operand, ctx); return (v < 0) ? -1 : !v; } case SEL_AND: { int l = eval_select(node->u.binop.left, ctx); if (l <= 0) return l; return eval_select(node->u.binop.right, ctx); } case SEL_OR: { int l = eval_select(node->u.binop.left, ctx); if (l > 0) return 1; if (l < 0) return l; return eval_select(node->u.binop.right, ctx); } case SEL_GT: { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return ctx_num(ctx, node->u.binop.left) > ctx_num(ctx, node->u.binop.right) ? 1 : 0; } case SEL_LT: { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return ctx_num(ctx, node->u.binop.left) < ctx_num(ctx, node->u.binop.right) ? 1 : 0; } case SEL_GE: { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return ctx_num(ctx, node->u.binop.left) >= ctx_num(ctx, node->u.binop.right) ? 1 : 0; } case SEL_LE: { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return ctx_num(ctx, node->u.binop.left) <= ctx_num(ctx, node->u.binop.right) ? 1 : 0; } case SEL_EQ: if (is_str_cmp(node)) { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return xce_str_match(ctx_str(ctx, node->u.binop.left), ctx_str(ctx, node->u.binop.right)); } { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return ctx_num(ctx, node->u.binop.left) == ctx_num(ctx, node->u.binop.right) ? 1 : 0; } case SEL_NE: if (is_str_cmp(node)) { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return !xce_str_match(ctx_str(ctx, node->u.binop.left), ctx_str(ctx, node->u.binop.right)) ? 1 : 0; } { int lr = eval_select(node->u.binop.left, ctx); int rr = eval_select(node->u.binop.right, ctx); if (lr < 0) return lr; if (rr < 0) return rr; return ctx_num(ctx, node->u.binop.left) != ctx_num(ctx, node->u.binop.right) ? 1 : 0; } } ERROR("eval_select_inner: unknown node type %d", (int)node->type); abort(); } /* ── GC ──────────────────────────────────────────────────────────────── */ /* Reconstruct the canonical intern-table key from a node pointer. * Applies the same commutativity sort as makeSelectNode. */ static sel_node_key_t sel_node_to_key(const xce_select_node_t* node) { uintptr_t op1 = 0, op2 = 0; switch (node->type) { case SEL_AND: case SEL_OR: case SEL_EQ: case SEL_NE: op1 = (uintptr_t)node->u.binop.left; op2 = (uintptr_t)node->u.binop.right; /* Same commutativity sort as makeSelectNode */ if (op1 > op2) { uintptr_t tmp = op1; op1 = op2; op2 = tmp; } break; case SEL_GT: case SEL_LT: case SEL_GE: case SEL_LE: op1 = (uintptr_t)node->u.binop.left; op2 = (uintptr_t)node->u.binop.right; break; case SEL_NOT: op1 = (uintptr_t)node->u.operand; break; case SEL_RESNAME: op1 = (uintptr_t)(uintptr_t)node->u.resname.res_idx; /* op2 = 0 (sel_make_key zeroes it for RESNAME) */ break; case SEL_NUM: { memcpy(&op1, &node->u.numval, sizeof(op1)); break; } case SEL_STR: op1 = (uintptr_t)node->u.strval; break; default: break; } return sel_make_key(node->type, op1, op2); } /* Clear all gc_mark bits in the intern table. * Called by the scheduler before the mark phase. */ void gc_clear_marks(void) { if (sel_node_table.table == NULL) return; int caps = sel_node_table.nodes.caps; for (int i = 0; i < caps; i++) { xce_select_node_t* node = (xce_select_node_t*)zarray_get(&sel_node_table.nodes, i); if (node == NULL) continue; node->gc_mark = 0; } } /* Mark all nodes reachable from root (recursive DFS). * Early-returns on NULL root or already-marked node to handle DAG sharing. * Recursion depth is bounded by SELECT_MAX_DEPTH (64). * Called by the scheduler to mark each live job's select tree. */ void gc_mark_select_tree(xce_select_node_t* node) { if (node == NULL || node->gc_mark) return; node->gc_mark = 1; switch (node->type) { case SEL_RESNAME: case SEL_NUM: case SEL_STR: case SEL_ANY: case SEL_TRUE: case SEL_FALSE: break; case SEL_NOT: gc_mark_select_tree(node->u.operand); break; default: gc_mark_select_tree(node->u.binop.left); gc_mark_select_tree(node->u.binop.right); break; } } /* Sweep phase of mark-and-sweep GC. Runs between scheduling sessions * (idle-phase invariant): no concurrent makeSelectNode or eval_select calls * are in progress. Does NOT acquire sel_node_table.lock — no contention at * idle phase. * * The mark phase (gc_clear_marks + gc_mark_select_tree per live node) must * be called by the scheduler BEFORE this function. The live_jobs and njobs * parameters are retained for API compatibility but are unused — the caller * owns the mark phase. */ void gc_sweep_select_nodes(const xce_sched_job_t** live_jobs, size_t njobs) { (void)live_jobs; (void)njobs; if (sel_node_table.table == NULL) { return; } int caps = sel_node_table.nodes.caps; /* Sweep: iterate all allocated slots (caps), skipping NULL gaps left * by prior zarray_del calls. For each unmarked node: remove its kv * entry, release xstring ref if SEL_STR, null the zarray slot via * zarray_del, then free the allocation. */ for (int i = 0; i < caps; i++) { xce_select_node_t* node = (xce_select_node_t*)zarray_get(&sel_node_table.nodes, i); if (node == NULL || node->gc_mark) continue; sel_node_key_t key = sel_node_to_key(node); key_del(sel_node_table.table, &key); if (node->type == SEL_STR && node->u.strval != NULL) { dereference(node->u.strval); } zarray_del(&sel_node_table.nodes, i); /* NULL slot before free */ free(node); } } /* Reassign node_id values to dense 0..N-1 and reset sel_node_next_id. * Called after gc_sweep_select_nodes to keep eval-context arrays bounded. */ void recompact_node_ids(void) { uint32_t next_id = 0; int caps = sel_node_table.nodes.caps; for (int i = 0; i < caps; i++) { xce_select_node_t* node = (xce_select_node_t*)zarray_get(&sel_node_table.nodes, i); if (node == NULL) continue; node->node_id = next_id++; } atomic_store_explicit(&sel_node_next_id, next_id, memory_order_release); } /* ── Phase 4d: const-fold transform ──────────────────────────────────────── */ /* Free a single node struct without touching its children. * Handles xstring deref for SEL_STR nodes. * Must not be called in CSE mode (level > 0) — intern table owns nodes. */ static void free_select_node(xce_select_node_t *n) { if (!n) return; if (n->type == SEL_STR && n->u.strval) dereference(n->u.strval); free(n); } xce_select_node_t * sel_const_fold_pass(xce_select_node_t *node) { if (node == NULL) return NULL; int level = sel_get_optimize_level(); switch (node->type) { /* ── SEL_AND ──────────────────────────────────────────────────────── */ case SEL_AND: { xce_select_node_t *left = sel_const_fold_pass(node->u.binop.left); xce_select_node_t *right = sel_const_fold_pass(node->u.binop.right); /* Update child pointers for OOM safety before any makeSelectNode. */ if (level == 0) { node->u.binop.left = left; node->u.binop.right = right; } /* Identity: TRUE && X → X */ if (left->type == SEL_TRUE) { if (level == 0) { free_select_node(node); free_select_node(left); } return right; } /* Identity (commutative): X && TRUE → X */ if (right->type == SEL_TRUE) { if (level == 0) { free_select_node(node); free_select_node(right); } return left; } /* Annihilation: FALSE && X → FALSE */ if (left->type == SEL_FALSE) { if (level == 0) { sel_free_tree(right); free_select_node(node); } return left; } /* Annihilation (commutative): X && FALSE → FALSE */ if (right->type == SEL_FALSE) { if (level == 0) { sel_free_tree(left); free_select_node(node); } return right; } return node; } /* ── SEL_OR ───────────────────────────────────────────────────────── */ case SEL_OR: { xce_select_node_t *left = sel_const_fold_pass(node->u.binop.left); xce_select_node_t *right = sel_const_fold_pass(node->u.binop.right); /* Update child pointers for OOM safety before any makeSelectNode. */ if (level == 0) { node->u.binop.left = left; node->u.binop.right = right; } /* Identity: FALSE || X → X */ if (left->type == SEL_FALSE) { if (level == 0) { free_select_node(node); free_select_node(left); } return right; } /* Identity (commutative): X || FALSE → X */ if (right->type == SEL_FALSE) { if (level == 0) { free_select_node(node); free_select_node(right); } return left; } /* Annihilation: TRUE || X → TRUE */ if (left->type == SEL_TRUE) { if (level == 0) { sel_free_tree(right); free_select_node(node); } return left; } /* Annihilation (commutative): X || TRUE → TRUE */ if (right->type == SEL_TRUE) { if (level == 0) { sel_free_tree(left); free_select_node(node); } return right; } return node; } /* ── SEL_NOT ──────────────────────────────────────────────────────── */ case SEL_NOT: { xce_select_node_t *operand = sel_const_fold_pass(node->u.operand); node->u.operand = operand; /* update first — fallback return node is valid */ /* Complement: !TRUE → FALSE */ if (operand->type == SEL_TRUE) { xce_select_node_t *result = makeSelectNode(SEL_FALSE, 0, 0); if (result == NULL) { return node; /* OOM fallback — safe in both modes: level==0 node is valid; level>0 interned node is never freed */ } if (level == 0) { free_select_node(node); free_select_node(operand); } return result; } /* Complement: !FALSE → TRUE */ if (operand->type == SEL_FALSE) { xce_select_node_t *result = makeSelectNode(SEL_TRUE, 0, 0); if (result == NULL) { return node; /* OOM fallback — safe in both modes: level==0 node is valid; level>0 interned node is never freed */ } if (level == 0) { free_select_node(node); free_select_node(operand); } return result; } return node; } /* ── SEL_EQ — fold EQ(any, x) or EQ(x, any) → SEL_TRUE ─────────── */ case SEL_EQ: { xce_select_node_t *left = sel_const_fold_pass(node->u.binop.left); xce_select_node_t *right = sel_const_fold_pass(node->u.binop.right); /* Update child pointers for OOM safety. */ if (level == 0) { node->u.binop.left = left; node->u.binop.right = right; } /* 4d-1: EQ with either operand SEL_ANY → SEL_TRUE */ if (left->type == SEL_ANY || right->type == SEL_ANY) { xce_select_node_t *result = makeSelectNode(SEL_TRUE, 0, 0); if (result == NULL) { return node; /* OOM fallback — safe in both modes: level==0 node is valid; level>0 interned node is never freed */ } if (level == 0) { sel_free_tree(left); sel_free_tree(right); free_select_node(node); } return result; } return node; } /* ── SEL_NE — fold NE(any, x) or NE(x, any) → SEL_FALSE ─────────── */ case SEL_NE: { xce_select_node_t *left = sel_const_fold_pass(node->u.binop.left); xce_select_node_t *right = sel_const_fold_pass(node->u.binop.right); /* Update child pointers for OOM safety. */ if (level == 0) { node->u.binop.left = left; node->u.binop.right = right; } /* 4d-1: NE with either operand SEL_ANY → SEL_FALSE */ if (left->type == SEL_ANY || right->type == SEL_ANY) { xce_select_node_t *result = makeSelectNode(SEL_FALSE, 0, 0); if (result == NULL) { return node; /* OOM fallback — safe in both modes: level==0 node is valid; level>0 interned node is never freed */ } if (level == 0) { sel_free_tree(left); sel_free_tree(right); free_select_node(node); } return result; } return node; } /* ── Non-associative comparison operators: recurse, no fold ──────── */ case SEL_GT: case SEL_LT: case SEL_GE: case SEL_LE: { xce_select_node_t *left = sel_const_fold_pass(node->u.binop.left); xce_select_node_t *right = sel_const_fold_pass(node->u.binop.right); if (level == 0) { node->u.binop.left = left; node->u.binop.right = right; } return node; } /* ── Leaf nodes: return as-is ────────────────────────────────────── */ case SEL_RESNAME: case SEL_NUM: case SEL_STR: case SEL_ANY: case SEL_TRUE: case SEL_FALSE: default: return node; } } /* ── Phase 4c: AND/OR flatten pass ────────────────────────────────────── * * Re-associates right-skewed AND/OR chains into left-skewed form: * A op (B op C) → (A op B) op C * * This is a single post-order pass. The caller loops until *changed == 0. * * Memory contract (level == 0): * When we replace node = AND(A, right) and right = AND(B, C), we: * 1. Build new_inner = AND(A, B) — new allocation * 2. Build new_outer = AND(new_inner, C) — new allocation * 3. free(right) — orphaned, its children B and C are re-parented * 4. free(node) — orphaned, its child A is re-parented * We never call free_select_tree() on an orphan — that would double-free * the re-parented children. * * Memory contract (level > 0, CSE): * makeSelectNode returns interned pointers owned by the GC table. * Never call free() on any node; GC will handle cleanup. */ xce_select_node_t * sel_flatten_pass(xce_select_node_t *node, int *changed) { if (node == NULL) return NULL; int level = sel_get_optimize_level(); switch (node->type) { /* ── Commutative binary operators subject to re-association ─────── */ case SEL_AND: case SEL_OR: { sel_node_type_t op = node->type; /* 1. Recurse into children first (post-order). */ xce_select_node_t *new_left = sel_flatten_pass(node->u.binop.left, changed); xce_select_node_t *new_right = sel_flatten_pass(node->u.binop.right, changed); /* OOM guard: propagate partial results without crashing. */ if (new_left == NULL) new_left = node->u.binop.left; if (new_right == NULL) new_right = node->u.binop.right; /* In level==0 (non-interned), update node's child pointers immediately * so that `node` is always a valid fallback if subsequent OOM occurs. * Recursive calls above already freed the old children when they built * replacements — leaving node->u.binop.* stale otherwise. */ if (level == 0) { node->u.binop.left = new_left; node->u.binop.right = new_right; } /* 2. Re-associate if right child has the same op: * node = op(A, op(B, C)) * Becomes: * new_outer = op(op(A, B), C) */ if (new_right->type == op) { xce_select_node_t *A = new_left; xce_select_node_t *B = new_right->u.binop.left; xce_select_node_t *C = new_right->u.binop.right; xce_select_node_t *new_inner = makeSelectNode(op, (uintptr_t)A, (uintptr_t)B); if (new_inner == NULL) { /* OOM: node's children are already updated above; safe to return. */ return node; } xce_select_node_t *new_outer = makeSelectNode(op, (uintptr_t)new_inner, (uintptr_t)C); if (new_outer == NULL) { /* OOM: free new_inner in level==0; node is still valid fallback. */ if (level == 0) free(new_inner); return node; } /* Free orphaned intermediate nodes in level==0 mode. * Only free the node struct itself — children are re-parented. */ if (level == 0) { free(new_right); free(node); } *changed = 1; return new_outer; } /* 3. Children changed but no re-association needed — rebuild the node * when children differ (required in level==0 because pointers are * unique; in CSE mode makeSelectNode may return the same pointer). */ int children_changed = (new_left != node->u.binop.left || new_right != node->u.binop.right); if (children_changed) { xce_select_node_t *rebuilt = makeSelectNode(op, (uintptr_t)new_left, (uintptr_t)new_right); /* OOM: node is valid (children already updated above). */ if (rebuilt == NULL) return node; if (level == 0) free(node); /* *changed was already set by the recursive call that produced the * changed child — no need to set it again. */ return rebuilt; } /* 4. No change at this level. */ return node; } /* ── Unary NOT: recurse on operand ─────────────────────────────── */ case SEL_NOT: { xce_select_node_t *new_op = sel_flatten_pass(node->u.operand, changed); if (new_op == NULL) new_op = node->u.operand; if (new_op != node->u.operand) { xce_select_node_t *rebuilt = makeSelectNode(SEL_NOT, (uintptr_t)new_op, 0); if (rebuilt == NULL) return node; if (level == 0) free(node); return rebuilt; } return node; } /* ── Non-associative binary operators: recurse but never swap ───── */ case SEL_GT: case SEL_LT: case SEL_GE: case SEL_LE: case SEL_EQ: case SEL_NE: { xce_select_node_t *new_left = sel_flatten_pass(node->u.binop.left, changed); xce_select_node_t *new_right = sel_flatten_pass(node->u.binop.right, changed); if (new_left == NULL) new_left = node->u.binop.left; if (new_right == NULL) new_right = node->u.binop.right; int children_changed = (new_left != node->u.binop.left || new_right != node->u.binop.right); if (children_changed) { xce_select_node_t *rebuilt = makeSelectNode(node->type, (uintptr_t)new_left, (uintptr_t)new_right); if (rebuilt == NULL) return node; if (level == 0) free(node); return rebuilt; } return node; } /* ── Leaf nodes: return as-is ────────────────────────────────────── */ case SEL_RESNAME: case SEL_NUM: case SEL_STR: case SEL_ANY: case SEL_TRUE: case SEL_FALSE: default: return node; } }