sel_key_cmp_fn compared keys with memcmp on the whole sel_node_key_t struct, including the 4-byte padding between type and op1. The stack- allocated search key in gc_sweep_select_nodes can have different padding bytes than the heap-allocated copy stored by key_add, causing key_del to miss the entry and leave a dangling pointer in the CSE intern table. Replace memcmp with field-by-field comparison on type, op1, op2. Also fix GC sweep tests that asserted pointer inequality after re-intern: calloc may lawfully reuse the freed address, so pointer inequality does not reliably indicate a successful sweep. Track the swept node's node_id before freeing and assert the re-interned node gets a fresh id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1345 lines
47 KiB
C
1345 lines
47 KiB
C
/* 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 <errno.h>
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <rtti.h>
|
|
|
|
#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;
|
|
}
|
|
}
|