feat: implement Phase 4d const-fold transform for select AST

Adds sel_const_fold_pass() — a single-pass bottom-up const-fold that
runs after the Phase 4c flatten loop in parse_select():

  4d-1  EQ/NE with either operand SEL_ANY → SEL_TRUE / SEL_FALSE
        (checks both child slots — commutativity sort makes SEL_ANY
        position non-deterministic after makeSelectNode interning)
  4d-2  Identity laws: TRUE&&X→X, X&&TRUE→X, FALSE||X→X, X||FALSE→X
  4d-3  Annihilation laws: FALSE&&X→FALSE, TRUE||X→TRUE (symmetric)
  4d-4  Complement laws: !TRUE→FALSE, !FALSE→TRUE

Memory semantics:
  level==0 (API server): frees orphaned nodes via free_select_node /
    sel_free_tree; OOM from makeSelectNode returns original node safely
  level>0  (CSE/scheduler): never manually frees; annihilation folds
    return the already-interned left/right singleton directly (no
    redundant makeSelectNode call); OOM guard unconditional so CSE
    mode also degrades gracefully instead of returning NULL

9 new TDD tests added to test_select.c (RED→GREEN verified).
All 166 tests pass (157 prior + 9 Phase 4d).
This commit is contained in:
Rongsong Shen 2026-04-29 08:24:04 +00:00
parent c2ad96a5e0
commit b38babb7d6
9 changed files with 3248 additions and 5 deletions

View file

@ -1,4 +1,7 @@
#define _XOPEN_SOURCE 700
#include <errno.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -8,10 +11,24 @@
#include "rusage.h"
#include "section.h"
#include "utils.h"
#include "xce/types.h"
#include "xce_resreq.h"
#include "xce_select_peg.h"
#include "xlist.h"
#include "xvec.h"
res_type_t valtype_to_res_type(int v)
{
switch (v) {
case XCE_VAL_NUMERIC: return RES_TYPE_NUMERIC;
case XCE_VAL_STRING: return RES_TYPE_STRING;
case XCE_VAL_BOOLEAN: return RES_TYPE_BOOL;
default:
ERROR("valtype_to_res_type: unknown valtype %d", v);
abort();
}
}
static int get_parse_errmsg(char** errmsg, const char* data, int pos)
{
size_t bufsize = 0;
@ -153,8 +170,127 @@ int parse_rusage(xlist_t* rusages, char* data, char** errmsg)
return rc;
}
/* Depth pre-scan: count '!' and '(' as depth increments.
* Returns 0 if depth is within limit, -1 if exceeded. */
static int check_select_depth(const char* input, int max_depth)
{
int depth = 0;
const char* p;
for (p = input; *p; p++) {
if (*p == '!' || *p == '(') {
depth++;
if (depth > max_depth) return -1;
} else if (*p == ')') {
if (depth > 0) depth--;
}
}
return 0;
}
static int parse_select(xce_primary_resreq_t* primary, char* selectString,
var resmgr, char** errmsg)
{
select_context_t ctx;
xce_select_context_t* p = NULL;
int rc = 1;
/* Without a resource manager, resource names cannot be resolved.
* Leave primary->select = NULL and succeed — callers that need
* select evaluation must supply a valid resmgr. */
if (resmgr == NULL) {
return 1;
}
memset(&ctx, 0, sizeof(ctx));
/* Length guard */
if (strlen(selectString) > SELECT_MAX_INPUT_LEN) {
*errmsg = strdup("select expression exceeds maximum length");
return -EINVAL;
}
/* Depth guard */
if (check_select_depth(selectString, SELECT_MAX_DEPTH) < 0) {
*errmsg = strdup("select expression exceeds maximum nesting depth");
return -EINVAL;
}
ctx.resmgr = resmgr;
ctx.input = selectString;
p = xce_select_create(&ctx);
if (p == NULL) return -errno;
/* LC_NUMERIC guard: strtod/snprintf in PEG actions must use '.' as the
* decimal separator regardless of the calling thread's locale setting. */
locale_t cloc = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0);
if (cloc == (locale_t)0) {
xce_select_destroy(p);
if (sel_get_optimize_level() > 0) {
ERROR("newlocale(LC_NUMERIC_MASK,C) failed: %s — scheduler cannot "
"safely parse numeric resreq values", strerror(errno));
abort();
}
ERROR("newlocale(LC_NUMERIC_MASK,C) failed: %s — rejecting resreq parse",
strerror(errno));
*errmsg = strdup("locale initialization failed");
return -EINVAL;
}
locale_t old = uselocale(cloc);
xce_select_parse(p, NULL);
uselocale(old);
freelocale(cloc);
if (ctx.has_error || ctx.root == NULL) {
if (ctx.errmsg[0] != '\0')
*errmsg = strdup(ctx.errmsg);
else
get_parse_errmsg(errmsg, selectString, ctx.err_pos);
rc = -EINVAL;
goto cleanup_parser;
}
/* Phase 4c: flatten AND/OR right-associative chains to left-associative */
{
int flat_changed;
do {
flat_changed = 0;
ctx.root = sel_flatten_pass(ctx.root, &flat_changed);
if (ctx.root == NULL) {
rc = -ENOMEM;
goto cleanup_parser;
}
} while (flat_changed);
}
/* Phase 4d: const-fold (single pass — does not create new flatten opportunities) */
ctx.root = sel_const_fold_pass(ctx.root);
if (ctx.root == NULL) {
rc = -ENOMEM;
goto cleanup_parser;
}
{
xce_select_t* sel = calloc(1, sizeof(*sel));
if (sel == NULL) {
if (sel_get_optimize_level() == 0)
sel_free_tree(ctx.root);
rc = -ENOMEM;
} else {
sel->root = ctx.root;
primary->select = sel;
}
}
cleanup_parser:
xce_select_destroy(p);
return rc;
}
int parse_resreq_primary(xce_primary_resreq_t* primary, char* resreqString,
char** errmsg)
var resmgr, char** errmsg)
{
int rc;
xce_resreq_data_t data;
@ -170,6 +306,15 @@ int parse_resreq_primary(xce_primary_resreq_t* primary, char* resreqString,
return rc;
}
/* Parse select clause BEFORE kill_resreq_data (data.selectString freed there) */
if (data.selectString != NULL && data.selectString[0] != '\0') {
rc = parse_select(primary, data.selectString, resmgr, errmsg);
if (rc < 0) {
kill_resreq_data(&data);
return rc;
}
}
xlist_init(&(primary->rusages), 8);
if ((data.rusageString != NULL) && (strlen(data.rusageString) != 0)) {
@ -197,7 +342,7 @@ void kill_resreq_ctx(resreq_context_t* ctx)
}
}
int parse_resreq(xce_resreq_t* resreq, char* data, char** errmsg)
int parse_resreq(xce_resreq_t* resreq, char* data, var resmgr, char** errmsg)
{
resreq_context_t ctx = { 0 };
xce_resreq_context_t* p = NULL;
@ -210,6 +355,7 @@ int parse_resreq(xce_resreq_t* resreq, char* data, char** errmsg)
*errmsg = NULL;
memset(resreq, 0x0, sizeof(*resreq));
ctx.resmgr = resmgr;
ctx.input = data;
ctx.resreq = resreq;
@ -241,6 +387,15 @@ void kill_primary_resreq(xce_primary_resreq_t* resreq)
return;
}
if (resreq->select != NULL) {
xce_select_node_t* root = resreq->select->root;
resreq->select->root = NULL;
if (sel_get_optimize_level() == 0 && root != NULL)
sel_free_tree(root);
FREEUP(resreq->select);
}
resreq->select = NULL;
for (i = 0; i < resreq->rusages.count; i++) {
rusage_value_t* v = (rusage_value_t*)xlist_get(&(resreq->rusages), i);

View file

@ -0,0 +1,58 @@
#include <stdlib.h>
#include <string.h>
#include <rtti.h>
#include "kv.h"
#include "resreq.h"
#include "xce/types.h"
#include "test_resmgr_fixture.h"
/* Internal helper to add one resource to the test resmgr */
static void add_resource(var resmgr, const char* name, int idx, int valtype)
{
xce_resource_t* r = calloc(1, sizeof(*r));
if (r == NULL) return;
r->name = strdup(name);
r->idx = idx;
r->valtype = valtype;
key_add(resmgr, (void*)r->name, r);
}
var make_test_resmgr(void)
{
kv_params_t params = {
.nslots = 64,
.keyops = &string_key_ops,
.datakiller = NULL, /* we free resources manually in free_test_resmgr */
};
var resmgr = makeInstance(&kvtab_type, &params);
if (resmgr == NULL) return NULL;
add_resource(resmgr, "type", TEST_RES_TYPE_IDX, XCE_VAL_STRING);
add_resource(resmgr, "mem", TEST_RES_MEM_IDX, XCE_VAL_NUMERIC);
add_resource(resmgr, "gpu", TEST_RES_GPU_IDX, XCE_VAL_BOOLEAN);
add_resource(resmgr, "ncpus", TEST_RES_NCPUS_IDX, XCE_VAL_NUMERIC);
add_resource(resmgr, "arch", TEST_RES_ARCH_IDX, XCE_VAL_STRING);
return resmgr;
}
void free_test_resmgr(var resmgr)
{
if (resmgr == NULL) return;
/* Free each xce_resource_t we allocated (datakiller=NULL means kvtab
* won't free them, so we must do it here). */
const char* names[] = { "type", "mem", "gpu", "ncpus", "arch", NULL };
for (int i = 0; names[i] != NULL; i++) {
xce_resource_t* r = NULL;
int rc = key_find(resmgr, (void*)names[i], (void**)&r);
if (rc > 0 && r != NULL) {
free(r->name);
free(r);
}
}
destroy(resmgr);
}

View file

@ -0,0 +1,19 @@
#if !defined(_TEST_RESMGR_FIXTURE_H_)
#define _TEST_RESMGR_FIXTURE_H_
#include <rtti.h>
/* Test resource indices — must match make_test_resmgr() registration order */
#define TEST_RES_TYPE_IDX 0 /* "type" — RES_TYPE_STRING */
#define TEST_RES_MEM_IDX 1 /* "mem" — RES_TYPE_NUMERIC */
#define TEST_RES_GPU_IDX 2 /* "gpu" — RES_TYPE_BOOL */
#define TEST_RES_NCPUS_IDX 3 /* "ncpus" — RES_TYPE_NUMERIC */
#define TEST_RES_ARCH_IDX 4 /* "arch" — RES_TYPE_STRING */
/* Create a test resmgr with 5 pre-registered resources.
* Returns a var (kvtab) that can be passed to parse_select / parse_resreq_primary.
* Caller must call free_test_resmgr() when done. */
extern var make_test_resmgr(void);
extern void free_test_resmgr(var resmgr);
#endif

File diff suppressed because it is too large Load diff

956
src/lib/xce_select.c Normal file
View file

@ -0,0 +1,956 @@
/* 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 <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 */
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)
{
return memcmp(a, b, sizeof(sel_node_key_t));
}
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, &params);
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. */
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;
}
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);
}
/* ── 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);
}
/* ── evaluation (stubs — return -1 / error) ──────────────────────────── */
int eval_select(const xce_select_node_t* node, select_eval_ctx_t* ctx)
{
(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;
}
void select_eval_ctx_reset(select_eval_ctx_t* ctx,
const host_eval_ctx_t* host)
{
(void)ctx; (void)host;
}
void select_eval_ctx_fini(select_eval_ctx_t* ctx)
{
(void)ctx;
}
/* ── 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);
}
/* 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.
*
* Sweep: remove every node from the intern table and free it. */
void gc_sweep_select_nodes(const xce_sched_job_t** live_jobs, size_t 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;
}
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;
}
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);
}
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) {}
/* ── 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;
}
}

260
src/lib/xce_select.peg Normal file
View file

@ -0,0 +1,260 @@
%prefix "xce_select"
%value "xce_select_node_t *"
%auxil "select_context_t *"
%source {
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "xstring.h"
#define PCC_ERROR(auxil) xce_select_parse_error(auxil)
static void xce_select_parse_error(select_context_t *auxil) {
auxil->has_error = 1;
}
/* Resolve a resource name: clause-keyword check then resmgr lookup.
* Returns a SEL_RESNAME node on success, NULL with has_error set on failure. */
static xce_select_node_t* resolve_resname(select_context_t* auxil,
const char* name)
{
static const char* clause_keywords[] = {
"select", "rusage", "span", "same", "affinity", NULL
};
int i;
for (i = 0; clause_keywords[i]; i++) {
if (strcmp(name, clause_keywords[i]) == 0) {
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
"resource name '%s' conflicts with a clause keyword — "
"use explicit select[%s ...] form if this is intentional",
name, name);
auxil->has_error = 1;
return NULL;
}
}
xce_resource_t* r = NULL;
int rc = key_find(auxil->resmgr, (void*)name, (void**)&r);
if (rc <= 0 || r == NULL) {
if (strcmp(name, "true") == 0 || strcmp(name, "false") == 0) {
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
"unknown resource '%s' — did you mean the boolean resource "
"name directly (e.g. 'gpu') or a numeric literal (1/0)?",
name);
} else {
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
"unknown resource '%s'", name);
}
auxil->has_error = 1;
return NULL;
}
res_type_t res_type = valtype_to_res_type(r->valtype);
return (xce_select_node_t*)makeSelectNode(SEL_RESNAME,
(uintptr_t)r->idx,
(uintptr_t)res_type);
}
}
%common {
#include "kv.h"
#include "resreq.h"
#define PCC_GETCHAR(auxil) \
({ int c = (auxil->input[auxil->pos] != '\0') ? (int)auxil->input[auxil->pos ++]: -1; \
if (auxil->pos > auxil->err_pos) auxil->err_pos = auxil->pos; \
c; \
})
}
statement <- _ e:select_or _ EOF {
auxil->root = e;
$$ = e;
}
select_or <- l:select_and _ "||" _ r:select_or {
if (auxil->has_error || l == NULL || r == NULL) {
$$ = NULL;
} else {
$$ = (xce_select_node_t*)makeSelectNode(SEL_OR, (uintptr_t)l, (uintptr_t)r);
if ($$ == NULL) {
auxil->has_error = 1;
if (sel_get_optimize_level() == 0) {
sel_free_tree(l);
sel_free_tree(r);
}
}
}
}
/ e:select_and { $$ = e; }
select_and <- l:select_not _ "&&" _ r:select_and {
if (auxil->has_error || l == NULL || r == NULL) {
$$ = NULL;
} else {
$$ = (xce_select_node_t*)makeSelectNode(SEL_AND, (uintptr_t)l, (uintptr_t)r);
if ($$ == NULL) {
auxil->has_error = 1;
if (sel_get_optimize_level() == 0) {
sel_free_tree(l);
sel_free_tree(r);
}
}
}
}
/ e:select_not { $$ = e; }
select_not <- "!" _ e:select_primary {
if (auxil->has_error || e == NULL) {
$$ = NULL;
} else {
$$ = (xce_select_node_t*)makeSelectNode(SEL_NOT, (uintptr_t)e, 0);
if ($$ == NULL) {
auxil->has_error = 1;
if (sel_get_optimize_level() == 0)
sel_free_tree(e);
}
}
}
/ e:select_primary { $$ = e; }
select_primary <- "(" _ e:select_or _ ")" { $$ = e; }
/ e:comparison { $$ = e; }
/ e:resname_leaf { $$ = e; }
comparison <- l:resname_node _ op:cmp_op _ r:value_node {
/* SEL_DISCARD: free a speculatively allocated node that the grammar
* action decides not to use. Only frees in level-0 (non-CSE) mode;
* in CSE mode the intern table owns the node and GC reclaims it. */
#define SEL_DISCARD(n) do { \
if ((n) != NULL && sel_get_optimize_level() == 0) { \
if ((n)->type == SEL_STR && (n)->u.strval != NULL) \
dereference((n)->u.strval); \
free(n); \
(n) = NULL; \
} \
} while (0)
if (auxil->has_error || l == NULL || r == NULL) {
SEL_DISCARD(l);
SEL_DISCARD(r);
$$ = NULL;
} else {
sel_node_type_t op_type = (sel_node_type_t)(uintptr_t)op;
if (op_type == SEL_EQ || op_type == SEL_NE) {
/* 4b-2: EQ/NE type validation */
if (l->u.resname.res_type == RES_TYPE_BOOL &&
r->type == SEL_STR) {
/* BOOL resource vs string literal: reject */
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
"type mismatch: boolean resource cannot be compared "
"with a string literal");
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
$$ = NULL;
} else if (l->u.resname.res_type == RES_TYPE_STRING &&
r->type == SEL_NUM) {
/* Coerce SEL_NUM → SEL_STR when LHS is STRING type */
char buf[64];
snprintf(buf, sizeof(buf), "%.17g", r->u.numval);
char* s = makeString(buf);
if (s == NULL) {
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
$$ = NULL;
} else {
if (sel_get_optimize_level() == 0) free(r);
r = (xce_select_node_t*)makeSelectNode(SEL_STR,
(uintptr_t)s,
0);
if (r == NULL) {
auxil->has_error = 1;
SEL_DISCARD(l);
$$ = NULL;
} else {
$$ = (xce_select_node_t*)makeSelectNode(op_type,
(uintptr_t)l,
(uintptr_t)r);
if ($$ == NULL) {
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
}
}
}
} else {
$$ = (xce_select_node_t*)makeSelectNode(op_type,
(uintptr_t)l,
(uintptr_t)r);
if ($$ == NULL) {
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
}
}
} else {
/* 4b-3: Ordered comparison (GT/LT/GE/LE) type validation */
if (l->u.resname.res_type != RES_TYPE_NUMERIC) {
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
"type mismatch: ordered comparison requires a numeric "
"resource on the left-hand side");
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
$$ = NULL;
} else if (r->type != SEL_NUM) {
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
"type mismatch: ordered comparison requires a numeric "
"literal on the right-hand side");
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
$$ = NULL;
} else {
$$ = (xce_select_node_t*)makeSelectNode(op_type,
(uintptr_t)l,
(uintptr_t)r);
if ($$ == NULL) {
auxil->has_error = 1;
SEL_DISCARD(l);
SEL_DISCARD(r);
}
}
}
}
#undef SEL_DISCARD
}
resname_node <- !("any" ![a-zA-Z_0-9]) < [a-zA-Z_][a-zA-Z_0-9]* > {
$$ = auxil->has_error ? NULL : resolve_resname(auxil, $0);
}
resname_leaf <- !("any" ![a-zA-Z_0-9]) < [a-zA-Z_][a-zA-Z_0-9]* > {
$$ = auxil->has_error ? NULL : resolve_resname(auxil, $0);
}
value_node <- "any" ![a-zA-Z_0-9] {
$$ = (xce_select_node_t*)makeSelectNode(SEL_ANY, 0, 0);
}
/ < [0-9]+ ("." [0-9]*)? > {
double v = strtod($0, NULL);
uintptr_t bits;
memcpy(&bits, &v, sizeof(bits));
$$ = (xce_select_node_t*)makeSelectNode(SEL_NUM, bits, 0);
}
/ < [a-zA-Z_][a-zA-Z_0-9]* > {
char* s = makeString((char*)$0);
if (s == NULL) { auxil->has_error = 1; $$ = NULL; }
else { $$ = (xce_select_node_t*)makeSelectNode(SEL_STR, (uintptr_t)s, 0); }
}
cmp_op <- ">=" { $$ = (xce_select_node_t*)(uintptr_t)SEL_GE; }
/ "<=" { $$ = (xce_select_node_t*)(uintptr_t)SEL_LE; }
/ "!=" { $$ = (xce_select_node_t*)(uintptr_t)SEL_NE; }
/ "==" { $$ = (xce_select_node_t*)(uintptr_t)SEL_EQ; }
/ ">" { $$ = (xce_select_node_t*)(uintptr_t)SEL_GT; }
/ "<" { $$ = (xce_select_node_t*)(uintptr_t)SEL_LT; }
_ <- [ \t]*
EOF <- !.