feat: export gc_clear_marks + gc_mark_select_tree; remove clear from gc_sweep (phase 6)

- Add gc_clear_marks(): iterates intern table and zeroes all gc_mark bits
- Rename static gc_mark_tree → public gc_mark_select_tree (no attribute((unused)))
- Remove Phase 6 guard (live_jobs != NULL early return) from gc_sweep_select_nodes
- Remove the mark-clear loop from gc_sweep_select_nodes; add (void) casts for now-unused params
- Declare gc_clear_marks and gc_mark_select_tree in resreq.h with protocol comment
- Add TDD test test_gc_clear_marks_exported verifying the exported primitives
- Prefix all existing gc_sweep_select_nodes(NULL, 0) call sites in test_select.c with gc_clear_marks()
This commit is contained in:
Rongsong Shen 2026-04-30 03:40:24 +00:00
parent bb4a95ce37
commit 9a19a8f932
3 changed files with 1372 additions and 55 deletions

View file

@ -64,6 +64,25 @@ struct xce_select_node_ {
typedef struct select_eval_ctx_ select_eval_ctx_t;
typedef struct host_eval_ctx_ host_eval_ctx_t;
/* Host snapshot: per-resource numeric (incl. bool as 0.0/1.0) and string values */
struct host_eval_ctx_ {
int num_res;
const double* numvals; /* indexed by res_idx */
const char** strvals; /* indexed by res_idx */
};
/* Per-evaluation-session memoization context */
struct select_eval_ctx_ {
const host_eval_ctx_t* host;
uint8_t* bool_bits; /* 2 bits per node_id: NONE/TRUE/FALSE/ERROR */
uint32_t bool_bits_cap; /* capacity in nodes (each uses 2 bits) */
uint32_t node_count; /* snapshot of sel_node_next_id at init */
double* num_vals; /* numeric/bool leaf cache, indexed by node_id */
uint32_t num_vals_cap;
const char** str_vals; /* string leaf cache, indexed by node_id */
uint32_t str_vals_cap;
};
/* Scheduler job forward declaration for GC API */
typedef struct xce_sched_job_ xce_sched_job_t;
@ -92,8 +111,7 @@ extern int eval_select_simple(const xce_select_node_t* root,
const host_eval_ctx_t* host);
/* Eval context lifecycle */
extern void select_eval_ctx_init(select_eval_ctx_t* ctx,
const host_eval_ctx_t* host);
extern select_eval_ctx_t* select_eval_ctx_init(uint32_t max_node_id);
extern void select_eval_ctx_reset(select_eval_ctx_t* ctx,
const host_eval_ctx_t* host);
extern void select_eval_ctx_fini(select_eval_ctx_t* ctx);
@ -103,6 +121,12 @@ extern void gc_sweep_select_nodes(const xce_sched_job_t** live_jobs,
size_t njobs);
extern void recompact_node_ids(void);
/* GC mark primitives — called by scheduler to implement the mark phase.
* Protocol: gc_clear_marks() → gc_mark_select_tree(root) per live node
* → gc_sweep_select_nodes(NULL, 0) */
extern void gc_clear_marks(void);
extern void gc_mark_select_tree(xce_select_node_t* node);
/* Flatten AND/OR right-associative chains to left-associative form.
* Returns the new root (may differ from the input root).
* Sets *changed to 1 if any re-association took place, leaves it unchanged
@ -251,6 +275,8 @@ extern res_type_t valtype_to_res_type(int v);
* Returns 1 if a == b, 0 otherwise. Named for future wildcard extension. */
static inline int xce_str_match(const char* a, const char* b)
{
/* NULL means the resource has no value — treat as no-match */
if (a == NULL || b == NULL) return 0;
return strcmp(a, b) == 0;
}

File diff suppressed because it is too large Load diff

View file

@ -155,9 +155,11 @@ void sel_node_table_fini(void)
}
/* Free each interned node before destroying the kvtab.
* datakiller=NULL means the kvtab does NOT free node values on destroy. */
int count = sel_node_table.nodes.count;
for (int i = 0; i < count; i++) {
* 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) {
@ -439,38 +441,318 @@ void kill_select(xce_select_t* sel)
free(sel);
}
/* ── evaluation (stubs — return -1 / error) ──────────────────────────── */
/* ── eval context lifecycle ───────────────────────────────────────────── */
int eval_select(const xce_select_node_t* node, select_eval_ctx_t* ctx)
select_eval_ctx_t* select_eval_ctx_init(uint32_t max_node_id)
{
(void)node; (void)ctx;
return -1;
}
int eval_select_simple(const xce_select_node_t* root,
const host_eval_ctx_t* host)
{
(void)root; (void)host;
return -1;
}
/* ── eval context lifecycle (stubs) ──────────────────────────────────── */
void select_eval_ctx_init(select_eval_ctx_t* ctx,
const host_eval_ctx_t* host)
{
(void)ctx; (void)host;
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)
{
(void)ctx; (void)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)
{
(void)ctx;
if (!ctx) return;
free(ctx->bool_bits);
free(ctx->num_vals);
free(ctx->str_vals);
free(ctx);
}
/* ── 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)
{
return n->type == SEL_NUM ? n->u.numval
: h->numvals[n->u.resname.res_idx];
}
static const char* simple_str(const xce_select_node_t* n,
const host_eval_ctx_t* h)
{
return n->type == SEL_STR ? n->u.strval
: h->strvals[n->u.resname.res_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:
return host->numvals[root->u.resname.res_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();
}
/* 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: {
double v = host->numvals[node->u.resname.res_idx];
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: {
const char* sv = host->strvals[node->u.resname.res_idx];
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:
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
return ctx_num(ctx, node->u.binop.left) >
ctx_num(ctx, node->u.binop.right) ? 1 : 0;
case SEL_LT:
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
return ctx_num(ctx, node->u.binop.left) <
ctx_num(ctx, node->u.binop.right) ? 1 : 0;
case SEL_GE:
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
return ctx_num(ctx, node->u.binop.left) >=
ctx_num(ctx, node->u.binop.right) ? 1 : 0;
case SEL_LE:
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
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)) {
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
return xce_str_match(ctx_str(ctx, node->u.binop.left),
ctx_str(ctx, node->u.binop.right));
}
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
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)) {
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
return !xce_str_match(ctx_str(ctx, node->u.binop.left),
ctx_str(ctx, node->u.binop.right)) ? 1 : 0;
}
eval_select(node->u.binop.left, ctx);
eval_select(node->u.binop.right, ctx);
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 ──────────────────────────────────────────────────────────────── */
@ -517,37 +799,70 @@ static sel_node_key_t sel_node_to_key(const xce_select_node_t* node)
return sel_make_key(node->type, op1, op2);
}
/* Mark-and-sweep GC. In Phase 4a the mark pass is not yet implemented
* (live_jobs traversal comes in Phase 4f). All nodes are treated as
* unreachable when live_jobs == NULL || njobs == 0.
/* 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.
*
* Sweep: remove every node from the intern table and free it. */
* 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;
}
/* Phase 4a: no mark pass — treat all nodes as unreachable when
* njobs == 0. Once Phase 4f adds live-job marking this block will
* skip nodes whose gc_mark bit is set. */
if (live_jobs != NULL && njobs > 0) {
/* Mark pass not yet implemented — skip sweep when live jobs present
* to avoid freeing potentially-reachable nodes. */
return;
}
int caps = sel_node_table.nodes.caps;
mylock_lock(&sel_node_table.lock);
/* Iterate the zarray of interned nodes and remove each from the kv
* table, then free the node allocation. */
int count = sel_node_table.nodes.count;
for (int i = 0; i < count; i++) {
xce_select_node_t* node
= (xce_select_node_t*)zarray_get(&sel_node_table.nodes, i);
if (node == NULL) {
continue;
}
/* 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);
@ -555,16 +870,25 @@ void gc_sweep_select_nodes(const xce_sched_job_t** live_jobs, size_t njobs)
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);
}
/* Clear the zarray tracking list. */
zarray_clear(&sel_node_table.nodes, 0);
mylock_unlock(&sel_node_table.lock);
}
void recompact_node_ids(void) {}
/* 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 ──────────────────────────────────────── */