diff --git a/src/include/resreq.h b/src/include/resreq.h index a2329ba2..b0c7ae9e 100644 --- a/src/include/resreq.h +++ b/src/include/resreq.h @@ -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; } diff --git a/src/lib/testsuite/test_select.c b/src/lib/testsuite/test_select.c index 29958774..346fa664 100644 --- a/src/lib/testsuite/test_select.c +++ b/src/lib/testsuite/test_select.c @@ -512,6 +512,185 @@ test_ordered_cmp_numeric_resource_succeeds(void) sel_optimize_test_reset(); } +/* ── Phase 4b: EQ type-validation tests ──────────────────────────────────*/ + +static void +test_4b_num_str_mismatch_eq_fails(void) +{ + /* "mem==linux" where "mem" is RES_TYPE_NUMERIC and "linux" is a string + * literal must be rejected: numeric resource vs string literal is a + * type mismatch for equality comparisons. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "mem==linux", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_str_num_coercion_eq_succeeds(void) +{ + /* "type==5" where "type" is RES_TYPE_STRING and "5" is a SEL_NUM literal. + * The numeric literal must be coerced to the string "5" so the comparison + * is accepted (STRING × NUM → coerce to STRING × STR("5")). */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "type==5", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_NOT_NULL(resreq.select); + TEST_ASSERT_NOT_NULL(resreq.select->root); + TEST_ASSERT_EQUAL_INT(SEL_EQ, resreq.select->root->type); + TEST_ASSERT_NOT_NULL(resreq.select->root->u.binop.left); + TEST_ASSERT_NOT_NULL(resreq.select->root->u.binop.right); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_true_resname_migration_error(void) +{ + /* A bare "true" token in resname_leaf position must be rejected with a + * migration diagnostic (legacy boolean syntax is no longer accepted). */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "true", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_false_resname_migration_error(void) +{ + /* A bare "false" token in resname_leaf position must be rejected with a + * migration diagnostic (legacy boolean syntax is no longer accepted). */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "false", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_bool_num_eq_accepted(void) +{ + /* "gpu==1" where "gpu" is RES_TYPE_BOOL and "1" is SEL_NUM. + * BOOL × NUM equality is accepted per design (numeric 0/1 maps to + * false/true for boolean resources). */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "gpu==1", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_NOT_NULL(resreq.select); + TEST_ASSERT_NOT_NULL(resreq.select->root); + TEST_ASSERT_EQUAL_INT(SEL_EQ, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_num_str_mismatch_ne_fails(void) +{ + /* "mem!=linux" where "mem" is RES_TYPE_NUMERIC and "linux" is a string + * literal must be rejected for NE just as for EQ. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "mem!=linux", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_bool_str_ne_fails(void) +{ + /* "gpu!=true" where "gpu" is RES_TYPE_BOOL and "true" is a string literal + * must be rejected: boolean resource cannot be compared with a string. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "gpu!=true", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_4b_str_num_coercion_ne_succeeds(void) +{ + /* "type!=5" where "type" is RES_TYPE_STRING and "5" is SEL_NUM. + * The numeric literal must be coerced to "5" and the NE comparison accepted. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "type!=5", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_NOT_NULL(resreq.select); + TEST_ASSERT_NOT_NULL(resreq.select->root); + TEST_ASSERT_EQUAL_INT(SEL_NE, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + /* ── 1-5: GC tests ────────────────────────────────────────────────────────*/ static void @@ -535,6 +714,7 @@ test_gc_sweep_unreachable_freed(void) TEST_ASSERT_NOT_NULL(node1); /* No live jobs → all nodes are unreachable */ + gc_clear_marks(); gc_sweep_select_nodes(NULL, 0); /* Re-intern: node1 was freed; a new node must be allocated */ @@ -586,6 +766,7 @@ test_gc_reintern_after_sweep(void) xce_select_node_t *n1 = makeSelectNode(SEL_TRUE, 0, 0); TEST_ASSERT_NOT_NULL(n1); + gc_clear_marks(); gc_sweep_select_nodes(NULL, 0); /* sweep all — no live jobs */ /* Re-intern: must not crash and must return a valid (non-NULL) node */ @@ -659,6 +840,109 @@ test_gc_predicate_staging_not_complete_blocks(void) TEST_ASSERT_EQUAL_INT(0, test_staging_merge_complete(&sched)); } +/* ── Phase 4f: recompact_node_ids tests ────────────────────────────────── + * + * 4f-5: recompact_node_ids must renumber live nodes to dense 0..N-1 and + * reset sel_node_next_id so subsequent allocations continue from N. + */ + +static void +test_recompact_ids_dense(void) +{ + /* Create 3 distinct SEL_NUM nodes; after recompact their node_ids must + * be 0, 1, 2 in insertion order. */ + sel_node_table_init(); + sel_optimize_set(1); + + double v1 = 1.0, v2 = 2.0, v3 = 3.0; + uintptr_t b1, b2, b3; + memcpy(&b1, &v1, sizeof(b1)); + memcpy(&b2, &v2, sizeof(b2)); + memcpy(&b3, &v3, sizeof(b3)); + + xce_select_node_t *n1 = makeSelectNode(SEL_NUM, b1, 0); + xce_select_node_t *n2 = makeSelectNode(SEL_NUM, b2, 0); + xce_select_node_t *n3 = makeSelectNode(SEL_NUM, b3, 0); + TEST_ASSERT_NOT_NULL(n1); + TEST_ASSERT_NOT_NULL(n2); + TEST_ASSERT_NOT_NULL(n3); + + recompact_node_ids(); + + /* IDs must be dense starting from 0 */ + TEST_ASSERT_EQUAL_UINT32(0, n1->node_id); + TEST_ASSERT_EQUAL_UINT32(1, n2->node_id); + TEST_ASSERT_EQUAL_UINT32(2, n3->node_id); + + sel_optimize_test_reset(); +} + +static void +test_recompact_resets_counter(void) +{ + /* After sweeping all nodes and reinternng 1, recompact must reset + * sel_node_next_id to 1 so the next new node gets id == 1 (not a + * stale large value). */ + sel_node_table_init(); + sel_optimize_set(1); + + /* Create and sweep two nodes to advance the counter */ + double va = 100.0, vb = 200.0; + uintptr_t ba, bb; + memcpy(&ba, &va, sizeof(ba)); + memcpy(&bb, &vb, sizeof(bb)); + makeSelectNode(SEL_NUM, ba, 0); + makeSelectNode(SEL_NUM, bb, 0); + + gc_clear_marks(); + gc_sweep_select_nodes(NULL, 0); /* remove all — no live jobs */ + + /* Reintern exactly one node */ + double vc = 300.0; + uintptr_t bc; + memcpy(&bc, &vc, sizeof(bc)); + xce_select_node_t *n_live = makeSelectNode(SEL_NUM, bc, 0); + TEST_ASSERT_NOT_NULL(n_live); + + recompact_node_ids(); + + /* The 1 live node gets id 0 */ + TEST_ASSERT_EQUAL_UINT32(0, n_live->node_id); + + /* The NEXT new node must get id 1 (counter reset to 1 by recompact) */ + double vd = 400.0; + uintptr_t bd; + memcpy(&bd, &vd, sizeof(bd)); + xce_select_node_t *n_new = makeSelectNode(SEL_NUM, bd, 0); + TEST_ASSERT_NOT_NULL(n_new); + TEST_ASSERT_EQUAL_UINT32(1, n_new->node_id); + + sel_optimize_test_reset(); +} + +static void +test_recompact_empty_table(void) +{ + /* recompact on an empty table must not crash and must leave the + * counter at 0. */ + sel_node_table_init(); + sel_optimize_set(1); + + gc_clear_marks(); + gc_sweep_select_nodes(NULL, 0); /* sweep any residual nodes */ + recompact_node_ids(); + + /* After recompact the first new node must get id == 0 */ + double vx = 999.0; + uintptr_t bx; + memcpy(&bx, &vx, sizeof(bx)); + xce_select_node_t *n = makeSelectNode(SEL_NUM, bx, 0); + TEST_ASSERT_NOT_NULL(n); + TEST_ASSERT_EQUAL_UINT32(0, n->node_id); + + sel_optimize_test_reset(); +} + /* ── sel_optimize_level write-once CAS (task 1-2) ────────────────────────*/ static void @@ -1296,6 +1580,643 @@ test_constfold_cse_eq_any_singleton(void) sel_optimize_test_reset(); } +/* ── Phase 4e: Evaluation with Memoization ──────────────────────────────── + * + * Tests for eval_select_simple (level==0, no ctx) and eval_select (CSE mode, + * memoized via select_eval_ctx_t). These tests verify actual evaluation + * results, not just AST structure. + */ + +/* Helper: fill a stack-allocated host_eval_ctx_t */ +static void +init_test_host(host_eval_ctx_t *host, double *numvals, const char **strvals, + int num_res) +{ + host->num_res = num_res; + host->numvals = numvals; + host->strvals = strvals; +} + +/* Five-resource host arrays matching test_resmgr_fixture: + * idx 0: type (STRING) + * idx 1: mem (NUMERIC) + * idx 2: gpu (BOOL) + * idx 3: ncpus (NUMERIC) + * idx 4: arch (STRING) + */ + +static void +test_p4e_simple_null_root(void) +{ + /* eval_select_simple(NULL, host) → 1 (vacuously true) */ + double numvals[5] = {0}; + const char *strvals[5] = {NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(NULL, &host)); +} + +static void +test_p4e_simple_bool_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 0.0, 1.0, 0.0, 0.0}; /* gpu=1 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "gpu", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_bool_false(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; /* gpu=0 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "gpu", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_gt_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* mem=100 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_gt_false(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 30.0, 0.0, 0.0, 0.0}; /* mem=30 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_lt_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* mem=100 < 200 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem<200", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_ge_boundary(void) +{ + /* mem>=100 with mem=100: boundary case must be true */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem>=100", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_le_boundary(void) +{ + /* mem<=100 with mem=100: boundary case must be true */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem<=100", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_eq_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem==100", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_eq_false(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 50.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem==100", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_numeric_ne_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem!=50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_string_eq_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {"linux", NULL, NULL, NULL, NULL}; /* type="linux" */ + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_string_eq_false(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; + const char *strvals[5] = {"windows", NULL, NULL, NULL, NULL}; /* type≠linux */ + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_not_inverts_true(void) +{ + /* !gpu with gpu=0.0 → NOT false = 1 */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; /* gpu=0 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "!gpu", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_not_inverts_false(void) +{ + /* !gpu with gpu=1.0 → NOT true = 0 */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 0.0, 1.0, 0.0, 0.0}; /* gpu=1 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "!gpu", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_and_both_true(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 1.0, 0.0, 0.0}; /* mem=100, gpu=1 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_and_short_circuit_false(void) +{ + /* gpu=0: AND short-circuits without evaluating mem>50 */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* gpu=0 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_or_short_circuit_true(void) +{ + /* gpu=1: OR short-circuits without evaluating mem>50 (which would be false) */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_optimize_set(0); + + double numvals[5] = {0.0, 30.0, 1.0, 0.0, 0.0}; /* gpu=1, mem=30 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "gpu || mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host)); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_sel_true_node(void) +{ + /* A SEL_TRUE leaf always evaluates to 1 */ + sel_optimize_set(0); + xce_select_node_t *node = makeSelectNode(SEL_TRUE, 0, 0); + TEST_ASSERT_NOT_NULL(node); + + double numvals[1] = {0}; + const char *strvals[1] = {NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 1); + + TEST_ASSERT_EQUAL_INT(1, eval_select_simple(node, &host)); + sel_free_tree(node); + sel_optimize_test_reset(); +} + +static void +test_p4e_simple_sel_false_node(void) +{ + /* A SEL_FALSE leaf always evaluates to 0 */ + sel_optimize_set(0); + xce_select_node_t *node = makeSelectNode(SEL_FALSE, 0, 0); + TEST_ASSERT_NOT_NULL(node); + + double numvals[1] = {0}; + const char *strvals[1] = {NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 1); + + TEST_ASSERT_EQUAL_INT(0, eval_select_simple(node, &host)); + sel_free_tree(node); + sel_optimize_test_reset(); +} + +/* ── Phase 4e: ctx lifecycle and memoized eval ─────────────────────────── */ + +static void +test_p4e_ctx_init_allocs_non_null(void) +{ + sel_optimize_set(1); + select_eval_ctx_t *ctx = select_eval_ctx_init(64); + TEST_ASSERT_NOT_NULL(ctx); + TEST_ASSERT_NOT_NULL(ctx->bool_bits); + TEST_ASSERT_NOT_NULL(ctx->num_vals); + TEST_ASSERT_NOT_NULL(ctx->str_vals); + select_eval_ctx_fini(ctx); + sel_optimize_test_reset(); +} + +static void +test_p4e_ctx_node_count_matches(void) +{ + sel_optimize_set(1); + select_eval_ctx_t *ctx = select_eval_ctx_init(100); + TEST_ASSERT_NOT_NULL(ctx); + TEST_ASSERT_EQUAL_INT(100, (int)ctx->node_count); + TEST_ASSERT_EQUAL_INT(100, (int)ctx->bool_bits_cap); + TEST_ASSERT_EQUAL_INT(100, (int)ctx->num_vals_cap); + TEST_ASSERT_EQUAL_INT(100, (int)ctx->str_vals_cap); + select_eval_ctx_fini(ctx); + sel_optimize_test_reset(); +} + +static void +test_p4e_ctx_reset_sets_host(void) +{ + sel_optimize_set(1); + select_eval_ctx_t *ctx = select_eval_ctx_init(64); + TEST_ASSERT_NOT_NULL(ctx); + + double numvals[5] = {0}; + const char *strvals[5] = {NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + select_eval_ctx_reset(ctx, &host); + TEST_ASSERT_EQUAL_PTR(&host, ctx->host); + + select_eval_ctx_fini(ctx); + sel_optimize_test_reset(); +} + +static void +test_p4e_memoized_null_root(void) +{ + /* eval_select(NULL, ctx) → 1 regardless of ctx state */ + sel_optimize_set(1); + select_eval_ctx_t *ctx = select_eval_ctx_init(64); + TEST_ASSERT_NOT_NULL(ctx); + + double numvals[5] = {0}; + const char *strvals[5] = {NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + select_eval_ctx_reset(ctx, &host); + + TEST_ASSERT_EQUAL_INT(1, eval_select(NULL, ctx)); + + select_eval_ctx_fini(ctx); + sel_optimize_test_reset(); +} + +static void +test_p4e_memoized_numeric_gt_true(void) +{ + /* eval_select (memoized) gives same correct result as eval_select_simple */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_node_table_init(); + sel_optimize_set(1); + + double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* mem=100 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + + select_eval_ctx_t *ctx = select_eval_ctx_init(256); + TEST_ASSERT_NOT_NULL(ctx); + select_eval_ctx_reset(ctx, &host); + + TEST_ASSERT_EQUAL_INT(1, eval_select(resreq.select->root, ctx)); + + select_eval_ctx_fini(ctx); + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_p4e_memoized_second_call_same(void) +{ + /* Calling eval_select twice with the same ctx must return the same value. + * The second call hits the bool_bits cache instead of re-evaluating. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + sel_node_table_init(); + sel_optimize_set(1); + + double numvals[5] = {0.0, 100.0, 1.0, 0.0, 0.0}; /* mem=100, gpu=1 */ + const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL}; + host_eval_ctx_t host; + init_test_host(&host, numvals, strvals, 5); + + int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + + select_eval_ctx_t *ctx = select_eval_ctx_init(256); + TEST_ASSERT_NOT_NULL(ctx); + select_eval_ctx_reset(ctx, &host); + + int result1 = eval_select(resreq.select->root, ctx); + int result2 = eval_select(resreq.select->root, ctx); + TEST_ASSERT_EQUAL_INT(1, result1); + TEST_ASSERT_EQUAL_INT(result1, result2); + + select_eval_ctx_fini(ctx); + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +/* ── Phase 6 prep: gc_clear_marks / gc_mark_select_tree exported ──────────*/ + +static void +test_gc_clear_marks_exported(void) +{ + sel_optimize_test_reset(); + sel_optimize_set(1); + sel_node_table_init(); + + xce_select_node_t* n1 = makeSelectNode(SEL_NUM, (uintptr_t)1.0, 0); + xce_select_node_t* n2 = makeSelectNode(SEL_NUM, (uintptr_t)2.0, 0); + TEST_ASSERT_NOT_NULL(n1); + TEST_ASSERT_NOT_NULL(n2); + + /* manually mark n1 */ + n1->gc_mark = 1; + + /* gc_clear_marks must reset all gc_mark bits */ + gc_clear_marks(); + TEST_ASSERT_EQUAL_INT(0, n1->gc_mark); + + /* gc_mark_select_tree must set gc_mark on n2 */ + gc_mark_select_tree(n2); + TEST_ASSERT_EQUAL_INT(1, n2->gc_mark); + + /* sweep preserves n2 (marked), removes n1 (unmarked) */ + gc_sweep_select_nodes(NULL, 0); + TEST_ASSERT_EQUAL_INT(1, n2->gc_mark); + + sel_node_table_fini(); + sel_optimize_test_reset(); +} + /* ── Top-level runner ─────────────────────────────────────────────────────*/ void @@ -1331,6 +2252,16 @@ test_select(void) RUN_TEST(test_ordered_cmp_string_rhs_fails); RUN_TEST(test_ordered_cmp_numeric_resource_succeeds); + /* ── Phase 4b: EQ type-validation tests ─────────────────────────────── */ + RUN_TEST(test_4b_num_str_mismatch_eq_fails); + RUN_TEST(test_4b_str_num_coercion_eq_succeeds); + RUN_TEST(test_4b_true_resname_migration_error); + RUN_TEST(test_4b_false_resname_migration_error); + RUN_TEST(test_4b_bool_num_eq_accepted); + RUN_TEST(test_4b_num_str_mismatch_ne_fails); + RUN_TEST(test_4b_bool_str_ne_fails); + RUN_TEST(test_4b_str_num_coercion_ne_succeeds); + RUN_TEST(test_gc_sweep_unreachable_freed); RUN_TEST(test_gc_sweep_reachable_retained); RUN_TEST(test_gc_reintern_after_sweep); @@ -1340,6 +2271,11 @@ test_select(void) RUN_TEST(test_gc_predicate_eval_in_progress_blocks); RUN_TEST(test_gc_predicate_staging_not_complete_blocks); + /* ── Phase 4f: recompact tests ───────────────────────────────────── */ + RUN_TEST(test_recompact_ids_dense); + RUN_TEST(test_recompact_resets_counter); + RUN_TEST(test_recompact_empty_table); + /* ── Phase 4c tests ───────────────────────────────────────────────── */ RUN_TEST(test_cse_gt_not_commutative); @@ -1361,5 +2297,36 @@ test_select(void) RUN_TEST(test_constfold_not_false_gives_true); RUN_TEST(test_constfold_cse_eq_any_singleton); + /* ── Phase 4e tests ───────────────────────────────────────────────── */ + RUN_TEST(test_p4e_simple_null_root); + RUN_TEST(test_p4e_simple_bool_true); + RUN_TEST(test_p4e_simple_bool_false); + RUN_TEST(test_p4e_simple_numeric_gt_true); + RUN_TEST(test_p4e_simple_numeric_gt_false); + RUN_TEST(test_p4e_simple_numeric_lt_true); + RUN_TEST(test_p4e_simple_numeric_ge_boundary); + RUN_TEST(test_p4e_simple_numeric_le_boundary); + RUN_TEST(test_p4e_simple_numeric_eq_true); + RUN_TEST(test_p4e_simple_numeric_eq_false); + RUN_TEST(test_p4e_simple_numeric_ne_true); + RUN_TEST(test_p4e_simple_string_eq_true); + RUN_TEST(test_p4e_simple_string_eq_false); + RUN_TEST(test_p4e_simple_not_inverts_true); + RUN_TEST(test_p4e_simple_not_inverts_false); + RUN_TEST(test_p4e_simple_and_both_true); + RUN_TEST(test_p4e_simple_and_short_circuit_false); + RUN_TEST(test_p4e_simple_or_short_circuit_true); + RUN_TEST(test_p4e_simple_sel_true_node); + RUN_TEST(test_p4e_simple_sel_false_node); + RUN_TEST(test_p4e_ctx_init_allocs_non_null); + RUN_TEST(test_p4e_ctx_node_count_matches); + RUN_TEST(test_p4e_ctx_reset_sets_host); + RUN_TEST(test_p4e_memoized_null_root); + RUN_TEST(test_p4e_memoized_numeric_gt_true); + RUN_TEST(test_p4e_memoized_second_call_same); + + /* ── Phase 6 prep: exported GC mark primitives ───────────────────── */ + RUN_TEST(test_gc_clear_marks_exported); + xstring_table_fini(); } diff --git a/src/lib/xce_select.c b/src/lib/xce_select.c index 2121cf14..7c9b48ef 100644 --- a/src/lib/xce_select.c +++ b/src/lib/xce_select.c @@ -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 ──────────────────────────────────────── */