From b38babb7d6c9401889a9c11aca3c818b439df228 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 29 Apr 2026 08:24:04 +0000 Subject: [PATCH] feat: implement Phase 4d const-fold transform for select AST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- src/docs/p1-2-host-select.md | 71 ++ src/docs/resreq-ast-cse-impl-plan.md | 220 ++++ src/include/resreq.h | 145 ++- src/lib/resreq.c | 159 ++- src/lib/testsuite/test_resmgr_fixture.c | 58 + src/lib/testsuite/test_resmgr_fixture.h | 19 + src/lib/testsuite/test_select.c | 1365 +++++++++++++++++++++++ src/lib/xce_select.c | 956 ++++++++++++++++ src/lib/xce_select.peg | 260 +++++ 9 files changed, 3248 insertions(+), 5 deletions(-) create mode 100644 src/docs/p1-2-host-select.md create mode 100644 src/docs/resreq-ast-cse-impl-plan.md create mode 100644 src/lib/testsuite/test_resmgr_fixture.c create mode 100644 src/lib/testsuite/test_resmgr_fixture.h create mode 100644 src/lib/testsuite/test_select.c create mode 100644 src/lib/xce_select.c create mode 100644 src/lib/xce_select.peg diff --git a/src/docs/p1-2-host-select.md b/src/docs/p1-2-host-select.md new file mode 100644 index 0000000..d1a0f16 --- /dev/null +++ b/src/docs/p1-2-host-select.md @@ -0,0 +1,71 @@ +# P1-2: Host Resource Requirement Select — Investigation + +## Status +- [>] Under investigation (moved from apiserver-backend-tasks.md) + +--- + +## Background + +The `select` field in an LSF-style resource requirement string (e.g. `select[mem>4096 && type==X86_64]`) +filters candidate hosts before slot allocation. Currently `EvalSelectForHost` is a no-CGO stub that +always returns `errNoCGO`. + +The full feature requires two layers: + +1. **C parser + evaluator** — parse the boolean select expression into an AST and evaluate it against + a host's resource values. +2. **Go bridge** — call the C evaluator via CGO and wire it into `FilterHostByResReqSelect` in the + controllers layer. + +--- + +## Sub-tasks + +### #11 P1-2a: C select expression parser (PEG + eval) + +**Files to touch:** +- `include/resreq.h` — Replace empty `xce_resreq_select_` stub with expression-tree types, enums, + and `extern` declarations for `parse_select`, `eval_select`, `free_select`. +- `lib/xce_select.peg` — New PEG grammar for select boolean expressions (right-recursive, handles + `&&`, `||`, `!`, comparison operators, numeric literals, string literals, resource names). +- `lib/resreq.c` — Implement `parse_select()`, `eval_select()`, `free_select()`; update + `kill_primary_resreq()` to call `free_select()`. +- `lib/meson.build` — Add `xce_select.peg` to explicit source list so the PEG tool generates C. +- `lib/testsuite/` — TDD tests for parse/eval before implementing. + +**Open questions:** +- Which PEG library is already available in the Nix/Meson build? (`peg`, `lemon`, `re2c`?) +- Exact grammar for comparison operators — LSF allows `mem > 4096`, `type == X86_64`, `swp >= 0`. + Need to confirm all supported operators and value types. +- Does `eval_select` receive a flat key-value map of resource values, or a pointer to the full + `xce_host_t` struct? +- Error reporting strategy — return a bool+error-string, or a tri-state (true/false/parse-error)? + +### #12 P1-2b: Go EvalSelectForHost + FilterHostByResReqSelect (blocked by #11) + +**Files to touch:** +- `backend/clib.go` — Implement `EvalSelectForHost(hostname, selectExpr string) error` using + `cgo.Handle` for callback context; add `#include "resreq.h"`, extern C declarations, + `xce_select_destroy` helper. +- `controllers/hosts.go` — Implement `FilterHostByResReqSelect(hosts []models.HostInfo, expr string)`, + update `preCheckHostRequest()` limit from 4094 → 1024 bytes. + +**Open questions:** +- Does `EvalSelectForHost` need to look up live resource values from the in-memory store, or are + values passed in directly by the C layer? +- `cgo.Handle` usage pattern — confirm whether a per-call handle or a session-level handle is needed. +- Input validation: max expression length (1024 bytes), allowed characters. + +--- + +## Investigation Checklist + +- [ ] Survey existing PEG tooling in the Meson/Nix build environment. +- [ ] Read `include/resreq.h` current state to understand `xce_resreq_select_` stub. +- [ ] Trace how `kill_primary_resreq()` works today and where `free_select()` must be called. +- [ ] Confirm resource value structure passed to `eval_select`. +- [ ] Confirm LSF select grammar spec (operators, value types, nesting depth). +- [ ] Prototype PEG grammar for a subset (numeric comparisons only) and get it compiling. +- [ ] Decide error propagation strategy across the CGO boundary. +- [ ] Write TDD tests for C parser before any implementation. diff --git a/src/docs/resreq-ast-cse-impl-plan.md b/src/docs/resreq-ast-cse-impl-plan.md new file mode 100644 index 0000000..6b927cd --- /dev/null +++ b/src/docs/resreq-ast-cse-impl-plan.md @@ -0,0 +1,220 @@ +# Resreq AST + CSE Implementation Plan + +> Generated from: `resreq-ast-cse-design.md` +> Date: 2026-04-28 +> Branch: merge_api_server + +--- + +## Summary Priority Order + +``` +P0 (blocking): makeString() TOCTOU fix, section.peg boundary bug ✓ DONE +Phase 1: TDD test skeletons (parallel with Phase 3) ✓ DONE +Phase 2: Header definitions + ABI audit ✓ DONE +Phase 3: PEG grammar (parallel with Phase 1) ✓ DONE 143 PASS / 3 FAIL (stubs) +Phase 4a: Core interning + sel_optimize_level ✓ DONE 145 PASS / 1 FAIL (Phase 4b stub) +Phase 4b: Type validation (gated on P0-1) ← CURRENT +Phase 4c/4d: Canonicalization + const-fold +Phase 4e: Memoized eval +Phase 4f: GC + recompaction +Phase 5: Build wiring +Phase 6: Scheduler integration (after all unit tests pass) +``` + +--- + +## P0 — Critical Prerequisites (blocking gate) + +Must complete before any Phase 4 work begins. + +| # | Task | Section | Rationale | +|---|------|---------|-----------| +| P0-1 | ~~**Fix `makeString()` TOCTOU in `src/lib/xstring.c`**~~ **DONE** — extended lock region to cover `makeInstance` + `key_add`, eliminating the TOCTOU window. `key_add` can no longer return `-EEXIST` on a miss path. | §5.4 | Silent NULL return corrupts the interned AST in all §7 numeric-to-string coercions; blocks Phase 4b entirely | +| P0-2 | ~~**Fix `section.peg` clause-boundary grammar**~~ **DONE** — Root cause: the original grammar used multiple ordered-choice alternatives in `select_statement` / `usage_statement` / `span_statement`, but PEG's `e*` always succeeds (matches empty), so alternatives 2+ were dead code (confirmed by packcc generating "defined but not used" for all `pcc_action_select_statement_N` functions). Fix: collapsed each statement rule into a single alternative with a compound negative lookahead covering all subsequent clause keywords. `select_statement` now stops at any of `rusage[`, `span[`, `same[`, `affinity[`, or `]`; `usage_statement` stops at `span[`, `same[`, `affinity[`, or `]`; `span_statement` stops at `same[`, `affinity[`, or `]`. Both implicit (`type==any span[ptile=1]`) and explicit (`select[type==any] span[ptile=1]`) select forms verified by TDD: `test_parse_section3` (implicit), `test_parse_section_explicit_select_with_span`, `test_parse_section_explicit_select_rusage_span`, `test_parse_section_span_not_in_select` — all PASS, no packcc dead-code warnings. | §3.1 | Silent span/rusage content bleed into selectString in all implicit-select + multi-clause inputs | + +--- + +## Phase 1 — TDD Tests (write failing skeletons first) ✓ DONE + +**Priority: HIGH — must come before implementation (TDD contract)** + +Develop concurrently with Phase 3 (grammar is the test oracle). + +| # | Task | Notes | +|---|------|-------| +| 1-1 | ~~Write `lib/testsuite/test_select.c` skeletons: parse valid expressions, unknown resource names (error), type mismatches (error)~~ **DONE** | | +| 1-2 | ~~Write tests: `SELECT_MAX_INPUT_LEN` (8192) overflow, `SELECT_MAX_DEPTH` (64) nesting overflow~~ **DONE** | | +| 1-3 | ~~Write CSE tests: `A&&B` and `B&&A` produce same pointer; two `makeSelectNode` calls with same args return same pointer~~ **DONE** — FAIL until Phase 4a | | +| 1-4 | ~~Write eval tests: numeric `>/<`/`==`, string `==`, bool standalone, `!`/`&&`/`||`, short-circuit~~ **DONE** | | +| 1-5 | ~~Write GC tests: unlink job root → `gc_sweep_select_nodes` frees unreachable node; subsequent `makeSelectNode` re-interns fresh node~~ **DONE** — FAIL until Phase 4f | | +| 1-6 | ~~Write GC predicate contract tests (`sched_session_active`, `sched_eval_in_progress`, `staging_merge_complete`) using `fake_scheduler_t` scaffold in `test_select.c`~~ **DONE** — FAIL until Phase 4f | Phase 6 wires real scheduler — signatures stable | +| 1-7 | ~~Create shared resmgr fixture: `src/lib/testsuite/test_resmgr_fixture.h` + `.c` with `make_test_resmgr()`/`free_test_resmgr()`~~ **DONE** | | + +--- + +## Phase 2 — Header Definitions ✓ DONE + +**Priority: HIGH — unblocks all implementation work** + +| # | Task | Notes | +|---|------|-------| +| 2-1 | ~~Add `res_type_t`, `sel_node_type_t`, `xce_select_node_t` typedef + struct to `include/resreq.h`~~ **DONE** | | +| 2-2 | ~~Fill `struct xce_resreq_select_` with `xce_select_node_t* root`~~ **DONE** | | +| 2-3 | ~~Declare all public APIs: `makeSelectNode`, `eval_select`, `eval_select_simple`, `select_eval_ctx_*`, `sel_node_table_*`, `gc_sweep_select_nodes`, `recompact_node_ids`~~ **DONE** | `gc_mark_tree` stays `static` in `.c` | +| 2-4 | ~~Add `var resmgr` field to `resreq_context_t` (outer PEG auxil)~~ **DONE** | | +| 2-5 | ~~Add `valtype_to_res_type` helper; omit `res_type_to_valtype` (no current consumer — YAGNI)~~ **DONE** | | +| 2-6 | ~~Add `xce_str_match` as `static inline` wrapping `strcmp`~~ **DONE** | | +| 2-7 | ~~**CGO audit**: `grep -rn 'parse_resreq\|C\.parse_resreq' src/frontend/`~~ **DONE** — no CGO callers found | | +| 2-8 | ~~Update `parse_resreq_primary` and `parse_resreq` signatures to accept `var resmgr`~~ **DONE** — call sites updated: `schedobj.c:171`, `test_resreq.c:244/301/330` | | + +--- + +## Phase 3 — PEG Grammar (`lib/xce_select.peg`) ✓ DONE + +**Priority: HIGH — develop concurrently with Phase 1** + +| # | Task | Notes | +|---|------|-------| +| 3-1 | ~~Implement `select_expr` grammar (§3.1): `\|\|`, `&&`, `!`, comparisons, resname, literals, `any`~~ **DONE** | | +| 3-2 | ~~Implement `select_context_t` auxil with `resmgr`, `depth`, `errmsg[256]`, `has_error`~~ **DONE** | | +| 3-3 | ~~Implement `resname` action: `key_find`, `res_idx` validity guard, `valtype_to_res_type` mapping~~ **DONE** | | +| 3-4 | ~~**Clause-keyword conflict check** in `resname` action: check against `{select, rusage, span, same, affinity}`~~ **DONE** | | +| 3-5 | ~~Implement `SELECT_MAX_INPUT_LEN` (8192) and `SELECT_MAX_DEPTH` (64) guards~~ **DONE** | | +| 3-6 | ~~LSF-compatible disambiguation: bare word vs known resource name → `SEL_RESNAME` vs `SEL_STR`; `any` excluded by grammar; `true`/`false` produce migration diagnostic~~ **DONE** | | +| 3-R1 | ~~**[Review fix — HIGH]** Switch `SEL_STR` from `strdup`/`free` to `makeString`/`dereference` in `xce_select.peg` and `xce_select.c`~~ **DONE** — xstring-interned strings now refcounted; `kill_select_node` calls `dereference()` | | +| 3-R2 | ~~**[Review fix — MEDIUM]** Add `_Static_assert(sizeof(uintptr_t) >= sizeof(double))` before `makeSelectNode` in `xce_select.c`~~ **DONE** — guards the `SEL_NUM` bit-cast at compile time | | +| 3-R3 | ~~**[Review fix — MEDIUM]** Add `!("any" ![a-zA-Z_0-9])` negative lookahead to `resname_node` and `resname_leaf` in `xce_select.peg`~~ **DONE** — prevents `any` keyword from being tried as a resource name | | + +--- + +## Phase 4a — Core Node Allocation and Interning ✓ DONE + +**Priority: HIGH — foundational; unblocks 4b–4f** + +| # | Task | Notes | +|---|------|-------| +| 4a-1 | ~~Implement `sel_optimize_level` (`static _Atomic int`, sentinel -1) with `sel_optimize_set()` write-once CAS and `sel_get_optimize_level()` accessor~~ **DONE** | | +| 4a-2 | ~~Implement `sel_make_key()` — zero padding bytes via struct literal init; SEL_RESNAME zeroes `op2` for key purposes (res_type stored in node, not in key)~~ **DONE** | | +| 4a-3 | ~~Implement `sel_key_hash` over the 3 key fields~~ **DONE** | | +| 4a-4 | ~~Implement `sel_node_table_init` / `sel_node_table_fini` — `makeInstance(&kvtab_type, ¶ms)` with **`datakiller=NULL`**, `zarray_init` for linear nodes array~~ **DONE** | | +| 4a-5 | ~~Implement `makeSelectNode` — CSE-disabled fast path; CSE-enabled path with commutativity sort, key lookup, TOCTOU double-check under lock, node_id assignment via `atomic_fetch_add`, zarray rollback via `atomic_fetch_sub`~~ **DONE** | | +| 4a-6 | ~~Implement `sel_node_to_key()` for GC key reconstruction (must apply same commutativity sort as `makeSelectNode`)~~ **DONE** | | +| 4a-7 | ~~Add `sel_optimize_test_reset()` test-only helper (`#ifdef UNIT_TEST`): `atomic_store_explicit` reset to -1 plus `sel_node_table_fini`~~ **DONE** | | +| 4a-R1 | ~~**[Review fix — HIGH]** Move `node_id` assignment inside lock after `key_add` succeeds so TOCTOU losers need no counter rollback~~ **DONE** | Counter divergence under concurrent load | +| 4a-R2 | ~~**[Review fix — HIGH]** Add `dereference((char*)op1)` on first-lock cache hit for SEL_STR — caller transfers xstring ownership~~ **DONE** | Ref leak on every cache hit for string nodes | +| 4a-R3 | ~~**[Review fix — HIGH]** Add `SEL_MAX_INTERNED_NODES` cap check in both lock sections (first miss + TOCTOU re-check)~~ **DONE** | Unbounded table growth without cap | +| 4a-R4 | ~~**[Review fix — HIGH]** Fix `sel_node_table_fini` to iterate and free all interned nodes before `destroy(table)` — NULL datakiller does not free them~~ **DONE** | All nodes leaked at teardown; LSAN clean now | +| 4a-R5 | ~~**[Review fix — MEDIUM]** Set `node->hashcode = sel_key_hash_fn(&key)` in CSE path — was left 0, breaking Phase 4e memoization~~ **DONE** | | +| 4a-R6 | ~~**[Review fix — MEDIUM]** Add `dereference((char*)op1)` on `calloc` failure in level==0 path~~ **DONE** | xstring ref leak on OOM | +| 4a-R7 | ~~**[Review fix — MEDIUM]** Add `-EINVAL` guard on `key_find` — corrupt table must not silently proceed~~ **DONE** | | +| 4a-R8 | ~~**[Review fix — MEDIUM]** Add `-EEXIST` recovery in `key_add` failure path — re-find and return the racing winner~~ **DONE** | Rare concurrent insert race | +| 4a-R9 | ~~**[Review fix — LOW]** Change `_Static_assert(sizeof(uintptr_t) >= ...)` to `==` per spec requirement~~ **DONE** | | + +--- + +## Phase 4b — Parse-Time Type Validation + +**Priority: HIGH — gate: P0-1 `makeString()` fix must be merged first** + +| # | Task | Notes | +|---|------|-------| +| 4b-1 | Wire `parse_select()` into `parse_resreq_primary` **before** `kill_resreq_data` | Use-after-free if called after `kill_resreq_data` frees `data.selectString` | +| 4b-2 | Implement EQ/NE type validation: reject BOOL×STR; coerce SEL_NUM to SEL_STR when comparing with string resource (`snprintf "%.17g"`), NULL-check `makeString` result, symmetric coercion | Free orphaned SEL_NUM node in unoptimized mode before overwriting pointer | +| 4b-3 | Implement `>/<`/`>=`/`<=` type validation: reject non-numeric operands | `log_fatal` (not `assert`) for belt-and-suspenders at eval time too | +| 4b-4 | Implement `res_idx` validity guard and unknown-resource error; add migration diagnostic for `true`/`false` identifiers (parse as SEL_RESNAME with no match) | | +| 4b-5 | Update `kill_primary_resreq` for conditional teardown: CSE-enabled → null root only; CSE-disabled → `free_select_tree()` then `FREEUP` | NULL store before `FREEUP` to prevent GC mark dangling-pointer race | +| 4b-6 | **LC_NUMERIC enforcement**: wrap PEG parser call in `parse_select` with `newlocale(LC_NUMERIC_MASK, "C")` / `uselocale` / `freelocale`; `log_fatal` on failure in scheduler, `log_error`+return in API server | `_XOPEN_SOURCE 700` required before system headers | +| 4b-7 | **strtod locale coverage check**: `grep -n 'strtod\|atof\|strtof' build/lib/xce_select.c` — if packcc inserts `strtod` in scanner, use `%0` capture + explicit `strtod` inside PEG action | Document finding (matches or no-matches) as a confirmed safe/unsafe result | + +--- + +## Phase 4c — Commutativity + Associativity Flattening + +| # | Task | Notes | +|---|------|-------| +| 4c-1 | Verify commutativity sort in `makeSelectNode` step 1 covers AND/OR/EQ/NE; non-commutative GT/LT/GE/LE/NOT unchanged | Already integrated in §5.4 design | +| 4c-2 | Implement post-parse **flatten pass** for `&&`/`||` chains: `do { changed = flatten_pass(root); } while (changed)` | Repeat-until-stable needed for depth-3+ explicit parenthesization | +| 4c-3 | In unoptimized mode: `flatten_pass` must `free()` each orphaned node — not its children (re-parented children must not be freed) | Children freed would cause double-free when `free_select_tree` walks from root | + +--- + +## Phase 4d — Const-Fold Transform + +| # | Task | Notes | +|---|------|-------| +| 4d-1 | `resname == any` → `SEL_TRUE`; `resname != any` → `SEL_FALSE` — check **both** child slots for SEL_ANY pointer (commutativity sort is ASLR-nondeterministic across runs) | | +| 4d-2 | Identity laws: `SEL_TRUE && X` → `X`; `SEL_FALSE \|\| X` → `X` | | +| 4d-3 | Annihilation laws: `SEL_FALSE && X` → `SEL_FALSE`; `SEL_TRUE \|\| X` → `SEL_TRUE` | | +| 4d-4 | Complement laws: `!SEL_TRUE` → `SEL_FALSE`; `!SEL_FALSE` → `SEL_TRUE` | Single-pass — const-fold shrinks trees, does not create new flattening opportunities | + +--- + +## Phase 4e — Evaluation with Memoization + +| # | Task | Notes | +|---|------|-------| +| 4e-1 | Implement `eval_get_bool`/`eval_set_bool` 2-bit bitmap accessors with `node_id >= bool_bits_cap` guards using `log_fatal` | `_Static_assert(EVAL_NONE == 0)` for memset-reset correctness | +| 4e-2 | Implement `eval_select_inner` — recursive eval with **explicit** error propagation (`if (lv < 0) return -1`), NOT C `&&`/`||` (C's `-1` is truthy, would swallow errors) | SEL_ANY reaching eval → `log_fatal("SEL_ANY reached eval — const-fold not applied")` | +| 4e-3 | Leaf cache writes in `eval_select_inner`: SEL_RESNAME writes to `ctx->num_vals[node_id]` or `ctx->str_vals[node_id]`; comparison ops call `eval_select(child, ctx)` then read cached arrays | **Critical**: `get_leaf_value()` must NOT be called directly — bypasses cache writes, silently reads 0.0/NULL | +| 4e-4 | Implement memoized `eval_select` wrapper — bitmap cache check before calling `eval_select_inner`; cache EVAL_ERROR too (node-structural errors repeat per host) | | +| 4e-5 | Implement `eval_select_simple` for unoptimized path — plain recursive tree-walk, no `select_eval_ctx_t`, thread-safe (no shared state) | | +| 4e-6 | Add `eval_select` guard: `log_fatal` if `sel_get_optimize_level() <= 0` | | +| 4e-7 | Implement `select_eval_ctx_init` / `select_eval_ctx_reset` / `select_eval_ctx_fini` | `node_count` snapshot at init; `reset` uses snapshot, never re-reads `sel_node_next_id` | +| 4e-8 | Pre-session capacity check: `memset` extended bool_bits bytes after realloc (realloc does not zero extension); partial-OOM contract — all three reallocs (bool_bits, num_vals, str_vals) must succeed atomically or `log_fatal` | Mismatched capacities allow out-of-bounds write to num_vals with no inline bounds check | + +--- + +## Phase 4f — Mark-and-Sweep GC + Recompaction + +| # | Task | Notes | +|---|------|-------| +| 4f-1 | Implement `gc_mark_tree` — recursive DFS, early return on `node == NULL \|\| node->gc_mark`; handles leaf / SEL_NOT / binary node types | Recursion bounded by `SELECT_MAX_DEPTH` (64); any increase requires stack analysis | +| 4f-2 | Implement `gc_sweep_select_nodes` — mark pass (clear all gc_mark + mark from live jobs dispatching on XCE_RESREQ_PRIMARY/COMPOUND/ALTER), sweep pass (collect unmarked, `key_del`, `dereference(strval)`, `free(node)`, `zarray_del`) | Use `xlist_get` with counted loop for xlist iteration; GC does NOT acquire `sel_node_table.lock` (idle-phase invariant) | +| 4f-3 | Insert **`atomic_thread_fence(memory_order_seq_cst)`** after all `primary->select->root = NULL` unlinks and before `gc_sweep_select_nodes` call | Without fence: CPU/compiler may reorder NULL store after GC mark pass | +| 4f-4 | Runtime GC idle invariant guards at GC call site (`sched_session_active`, `sched_eval_in_progress`, `staging_merge_complete`) — `log_fatal` not `assert` | `assert` compiled out by NDEBUG; use-after-free would result | +| 4f-5 | Implement `recompact_node_ids` — iterate `nodes` zarray over `caps` slots (not `count`, gaps exist after `zarray_del`), assign sequential `node_id`, reset `sel_node_next_id` via `atomic_store_explicit(..., memory_order_release)` | `node_id` and zarray slot index are **decoupled** — do not conflate | + +--- + +## Phase 5 — Build Wiring + +| # | Task | Notes | +|---|------|-------| +| 5-1 | Add `custom_target` for `xce_select.peg` in `lib/meson.build` matching `section_parser`/`rusage_parser` pattern | Verify `-o 'lib/xce_select'` path format: `grep -A8 'custom_target' src/lib/meson.build \| grep '\-o'` | +| 5-2 | Add `xce_select_parser[0]` (generated `.c`) to `libxce_src` | | +| 5-3 | Add `test_resmgr_fixture.c` to testsuite `executable()` sources in `lib/meson.build` | | + +--- + +## Phase 6 — Scheduler Integration + +**Priority: MEDIUM — after Phases 1–5 pass all tests** + +| # | Task | Notes | +|---|------|-------| +| 6-1 | **Pre-Phase-6 audit**: `grep -rn 'eval_select\|alloc_thread\|sched_session' src/scheduler/` — confirm allocator-thread-only invariant | Document result ("confirmed single-threaded" or "found unexpected call site at [location]") in commit message | +| 6-2 | **Blocking gate**: `grep -rn 'sched_meet_host_selection\|meet_host_selection' src/` — all hits must be accounted for before signature change | Missed call site → wrong ABI → UB at runtime (garbage stack parameter) | +| 6-3 | Add `_Atomic int session_active`, `eval_in_progress`, `staging_merge_complete` to `xce_scheduler_t` in `src/scheduler/include/scheduler.h` | Use `_Atomic int` not `volatile int` — ARM atomicity guarantee | +| 6-4 | Implement GC predicate wrappers referencing real `xce_scheduler_t` fields (replace Phase 1 `fake_scheduler_t` stubs) | Signatures unchanged; only wrapper body changes | +| 6-5 | Restructure scheduler session loop to **outer-host / inner-job** pattern with single `select_eval_ctx_t` per session | Current stub returns 1 — full from-scratch implementation | +| 6-6 | Update `sched_meet_host_selection` to `void` return; migrate `add_job_preason(PREASON_MATCH_FAIL)` into inner-job loop | Deleting the preason call silently leaves jobs appearing to hang | +| 6-7 | Implement `build_host_eval_ctx` (scheduler-private in `src/scheduler/src/`) — flatten `worker->resvals` zarray into `numvals`/`strvals` arrays using `Value_u_fvalue`/`Value_u_svalue`/`Value_u_bvalue` from capnp-generated header | Confirm header location: `grep -rn 'Value_u_fvalue' src/scheduler/` | +| 6-8 | Materialize `live_jobs` flat array from `sched->jobs` kvmgr using growth-loop heap allocation (not fragile `zarray_t.data` cast) | | +| 6-9 | Wire `sel_optimize_set(1)` at scheduler startup; `sel_optimize_set(0)` at API server startup | Add `log_fatal` guard at session loop entry if `sel_get_optimize_level() <= 0` | +| 6-10 | Set `sel_alloc_thread_id` at allocator thread first iteration; add thread-ID checks in `eval_select` and `gc_sweep_select_nodes` | Skip check when `sel_alloc_thread_id == 0` (unregistered — Phase 1–5 unit tests) | +| 6-11 | Write integration/parity test against known-good resreq/host pairs | Stub always returns 1, so no meaningful baseline exists — must test explicitly | + +--- + +## Key Implementation Traps + +1. **`free_select_tree` vs GC mode confusion** — calling `free_select_tree` in CSE-enabled mode or forgetting it in disabled mode both produce memory corruption. The `sel_get_optimize_level()` guard inside `free_select_tree` catches this with `log_fatal`. + +2. **`get_leaf_value()` bypass bug** — calling `get_leaf_value()` directly in comparison branches bypasses the `num_vals`/`str_vals` cache write. Comparisons silently read 0.0/NULL (calloc default) instead of actual host values. Every comparison operator must call `eval_select(child, ctx)` first, then read the typed cache array. + +3. **TOCTOU in `makeSelectNode`** — the double-check under lock (step 5) prevents node leaks when two threads race to intern the same node. The re-find must happen inside the lock or the winner could be freed before the loser reads it. + +4. **`atomic_thread_fence` before GC** — root-unlink writes (`primary->select->root = NULL`) must be visible to the GC mark pass. Without a seq_cst fence, CPU/compiler reordering causes GC to retain a node that should be swept (or vice versa). + +5. **Partial-OOM in `select_eval_ctx_t` resize** — if one of the three reallocs (bool_bits, num_vals, str_vals) succeeds and another fails, the capacities diverge and `eval_select_inner` can write out-of-bounds to `num_vals` (no inline bounds guard at write site). Treat all three as atomic — `log_fatal` on partial failure. diff --git a/src/include/resreq.h b/src/include/resreq.h index 5cecfbd..a2329ba 100644 --- a/src/include/resreq.h +++ b/src/include/resreq.h @@ -2,14 +2,121 @@ #define _RESREQ_H_ 1 +#include #include #include #include +#include #include "kvmgr.h" #include "xlist.h" #include "xvec.h" +/* ────────────────────────────────────────────────────────────────────────── + * Select-expression AST (Phase 3-4 implementation) + * ────────────────────────────────────────────────────────────────────────── */ + +typedef enum { + SEL_AND, /* && — commutative binary */ + SEL_OR, /* || — commutative binary */ + SEL_NOT, /* ! — unary */ + SEL_GT, /* > — numeric only */ + SEL_LT, /* < — numeric only */ + SEL_GE, /* >= — numeric only */ + SEL_LE, /* <= — numeric only */ + SEL_EQ, /* == — commutative, all types */ + SEL_NE, /* != — commutative, all types */ + SEL_RESNAME, /* leaf: resource variable */ + SEL_NUM, /* leaf: numeric literal */ + SEL_STR, /* leaf: string literal (xstring-interned) */ + SEL_ANY, /* leaf: 'any' keyword — eliminated by const-fold */ + SEL_TRUE, /* leaf: const true — produced by const-fold only */ + SEL_FALSE, /* leaf: const false — produced by const-fold only */ +} sel_node_type_t; + +typedef enum { + RES_TYPE_NUMERIC = 0, /* float — supports all comparison operators */ + RES_TYPE_STRING, /* char* — supports == != only */ + RES_TYPE_BOOL, /* int — standalone boolean use */ +} res_type_t; + +typedef struct xce_select_node_ xce_select_node_t; +struct xce_select_node_ { + sel_node_type_t type; + uint32_t hashcode; + uint32_t node_id; + unsigned gc_mark; + union { + struct { + xce_select_node_t *left, *right; + } binop; /* AND / OR / GT / LT / GE / LE / EQ / NE */ + xce_select_node_t* operand; /* NOT */ + struct { + int res_idx; + res_type_t res_type; + } resname; + double numval; /* SEL_NUM */ + char* strval; /* SEL_STR — xstring-interned */ + } u; +}; + +/* Evaluation context forward declarations */ +typedef struct select_eval_ctx_ select_eval_ctx_t; +typedef struct host_eval_ctx_ host_eval_ctx_t; + +/* Scheduler job forward declaration for GC API */ +typedef struct xce_sched_job_ xce_sched_job_t; + +/* makeSelectNode — intern or allocate a select AST node. + * Returns NULL on OOM or intern-table cap exceeded. */ +extern xce_select_node_t* makeSelectNode(sel_node_type_t type, + uintptr_t op1, uintptr_t op2); + +/* Optimization level — set once at process startup. + * 0 = disabled (API server); >0 = CSE+memoization (scheduler). */ +extern void sel_optimize_set(int level); +extern int sel_get_optimize_level(void); + +/* Intern table lifecycle */ +extern void sel_node_table_init(void); +extern void sel_node_table_fini(void); + +/* Test-only reset (requires #define UNIT_TEST before this header) */ +#ifdef UNIT_TEST +extern void sel_optimize_test_reset(void); +#endif + +/* Evaluation APIs */ +extern int eval_select(const xce_select_node_t* node, select_eval_ctx_t* ctx); +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 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); + +/* Mark-and-sweep GC between scheduling sessions */ +extern void gc_sweep_select_nodes(const xce_sched_job_t** live_jobs, + size_t njobs); +extern void recompact_node_ids(void); + +/* 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 + * otherwise (callers must initialise *changed to 0 before the first call). + * In level==0 mode: frees orphaned intermediate nodes (NOT their re-parented + * children). In CSE mode (level > 0): no explicit freeing — GC owns nodes. */ +extern xce_select_node_t* sel_flatten_pass(xce_select_node_t* root, + int* changed); + +/* Single-pass const-fold transform (Phase 4d). + * In level==0: may return a new allocation; frees consumed subtrees. + * In CSE mode (level > 0): returns an interned singleton; GC owns nodes. */ +extern xce_select_node_t* sel_const_fold_pass(xce_select_node_t* node); + typedef struct xce_resreq_select_ xce_select_t; typedef struct xce_resreq_rusages_ xce_rusages_t; typedef struct xce_resreq_span_ xce_span_t; @@ -34,6 +141,23 @@ typedef struct { xce_resreq_data_t data; } section_context_t; +/* Maximum allowed select expression length and nesting depth */ +#define SELECT_MAX_INPUT_LEN 8192 +#define SELECT_MAX_DEPTH 64 + +/* Parse context for xce_select.peg — auxil struct for packcc-generated parser */ +typedef struct select_context_t_ select_context_t; +struct select_context_t_ { + var resmgr; /* resource manager for name resolution */ + const char* input; + int pos; + int err_pos; + int has_error; + char errmsg[256]; /* fixed-size — no alloc/free in grammar actions */ + int depth; /* current nesting depth — guard vs SELECT_MAX_DEPTH */ + xce_select_node_t* root; /* result root set by grammar top-level action */ +}; + typedef enum { PER_TASK = 0, PER_JOB, @@ -64,7 +188,9 @@ typedef struct { int decay_flag; } rusage_value_t; -struct xce_resreq_select_ { }; +struct xce_resreq_select_ { + xce_select_node_t* root; +}; struct xce_resreq_span_ { }; @@ -106,6 +232,7 @@ typedef struct { } xce_resreq_t; typedef struct { + var resmgr; const char* input; int pos; int err_pos; @@ -118,13 +245,25 @@ typedef struct { int resreq_fact_count; } resreq_context_t; +extern res_type_t valtype_to_res_type(int v); + +/* Single comparison point for string-valued resources. + * Returns 1 if a == b, 0 otherwise. Named for future wildcard extension. */ +static inline int xce_str_match(const char* a, const char* b) +{ + return strcmp(a, b) == 0; +} + +extern void kill_select(xce_select_t*); +extern void sel_free_tree(xce_select_node_t*); extern void kill_section_ctx(section_context_t*); extern void kill_resreq_data(xce_resreq_data_t*); extern int parse_section(xce_resreq_data_t*, char*, char**); extern int parse_rusage(xlist_t*, char*, char**); extern void kill_rusage_value(rusage_value_t*); -extern int parse_resreq_primary(xce_primary_resreq_t*, char*, char**); -extern int parse_resreq(xce_resreq_t*, char*, char**); +extern int parse_resreq_primary(xce_primary_resreq_t*, char*, var, char**); +extern void kill_primary_resreq(xce_primary_resreq_t*); +extern int parse_resreq(xce_resreq_t*, char*, var, char**); extern void kill_resreq(xce_resreq_t*); extern int format_resreq(char**, xce_resreq_t*); extern int calc_wanted_global_resvals(xvec_idval_t*, var, xlist_t*, diff --git a/src/lib/resreq.c b/src/lib/resreq.c index 752675a..8b6bfc2 100644 --- a/src/lib/resreq.c +++ b/src/lib/resreq.c @@ -1,4 +1,7 @@ +#define _XOPEN_SOURCE 700 + #include +#include #include #include #include @@ -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); diff --git a/src/lib/testsuite/test_resmgr_fixture.c b/src/lib/testsuite/test_resmgr_fixture.c new file mode 100644 index 0000000..ed60a19 --- /dev/null +++ b/src/lib/testsuite/test_resmgr_fixture.c @@ -0,0 +1,58 @@ +#include +#include + +#include + +#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, ¶ms); + 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); +} diff --git a/src/lib/testsuite/test_resmgr_fixture.h b/src/lib/testsuite/test_resmgr_fixture.h new file mode 100644 index 0000000..b825bad --- /dev/null +++ b/src/lib/testsuite/test_resmgr_fixture.h @@ -0,0 +1,19 @@ +#if !defined(_TEST_RESMGR_FIXTURE_H_) +#define _TEST_RESMGR_FIXTURE_H_ + +#include + +/* 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 diff --git a/src/lib/testsuite/test_select.c b/src/lib/testsuite/test_select.c new file mode 100644 index 0000000..2995877 --- /dev/null +++ b/src/lib/testsuite/test_select.c @@ -0,0 +1,1365 @@ +/* test_select.c — Phase 1 TDD skeletons for select-expression AST / CSE. + * + * Compile but fail until Phases 3-4 implement the full select pipeline. + * The UNIT_TEST macro must be defined before including resreq.h so that + * sel_optimize_test_reset() is visible. + */ +#define UNIT_TEST 1 + +#include +#include +#include +#include +#include + +#include + +#include "resreq.h" +#include "test_resmgr_fixture.h" +#include "utils.h" +#include "xstring.h" + +/* ── fake_scheduler_t ───────────────────────────────────────────────────── + * + * Phase 1 stub. Phase 6 replaces these wrappers with equivalents that + * reference real xce_scheduler_t fields — the atomic field layout and + * predicate semantics are stable. + */ +typedef struct { + _Atomic int session_active; + _Atomic int eval_in_progress; + _Atomic int staging_merge_complete; +} fake_scheduler_t; + +static int +test_sched_session_active(fake_scheduler_t *s) +{ + return atomic_load_explicit(&s->session_active, memory_order_acquire); +} + +static int +test_sched_eval_in_progress(fake_scheduler_t *s) +{ + return atomic_load_explicit(&s->eval_in_progress, memory_order_acquire); +} + +static int +test_staging_merge_complete(fake_scheduler_t *s) +{ + return atomic_load_explicit(&s->staging_merge_complete, + memory_order_acquire); +} + +/* ── 1-1: Parse tests ─────────────────────────────────────────────────────*/ + +static void +test_select_parse_valid_expr(void) +{ + /* parse_resreq_primary with a well-formed select expression must + * succeed (rc >= 0) and produce a non-NULL root node. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + /* resmgr fixture is created for teardown symmetry; Phase 2 threads it + * into parse_resreq_primary — for Phase 1 we use the 3-arg signature. */ + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg); + + /* Phase 4: expect success */ + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_NOT_NULL(resreq.select); + TEST_ASSERT_NOT_NULL(resreq.select->root); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_select_parse_unknown_resource(void) +{ + /* A resource name that is not present in the resmgr must produce a + * parse error (rc < 0). */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "unknownres==linux", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_select_parse_type_mismatch(void) +{ + /* Comparing a BOOL resource with a string literal is a type mismatch + * and must be rejected. "gpu" is registered as RES_TYPE_BOOL in the + * fixture; "gpu==true" supplies a string literal, not a bare bool. */ + 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(); +} + +/* ── 1-2: Overflow guard tests ────────────────────────────────────────────*/ + +static void +test_select_input_len_overflow(void) +{ + /* Expressions longer than SELECT_MAX_INPUT_LEN (8192) must be rejected + * immediately — before any parsing work is done. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + const int limit = 8192 + 1; + char * longexpr = malloc((size_t)limit + 1); + TEST_ASSERT_NOT_NULL(longexpr); + memset(longexpr, 'a', (size_t)limit); + longexpr[limit] = '\0'; + + int rc = parse_resreq_primary(&resreq, longexpr, resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + free(longexpr); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_select_depth_overflow(void) +{ + /* Nesting deeper than SELECT_MAX_DEPTH (64) must be rejected. + * Build 65 levels of '!' prefix to exceed the limit. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + const int max_depth = 65; + /* Each level is one '!' character; add "gpu" plus NUL at the end. */ + char *deepexpr = malloc((size_t)max_depth + 4); + TEST_ASSERT_NOT_NULL(deepexpr); + int pos = 0; + for (int i = 0; i < max_depth; i++) + deepexpr[pos++] = '!'; + deepexpr[pos++] = 'g'; + deepexpr[pos++] = 'p'; + deepexpr[pos++] = 'u'; + deepexpr[pos] = '\0'; + + int rc = parse_resreq_primary(&resreq, deepexpr, resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + free(deepexpr); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +/* ── 1-3: CSE / hash-consing tests ───────────────────────────────────────*/ + +static void +test_select_cse_commutative_same_ptr(void) +{ + /* When CSE is enabled, A&&B and B&&A must be canonicalised to the same + * interned pointer because && is commutative. */ + sel_node_table_init(); + sel_optimize_set(1); + + xce_select_node_t *a = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, 0); + xce_select_node_t *b = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_MEM_IDX, 0); + TEST_ASSERT_NOT_NULL(a); + TEST_ASSERT_NOT_NULL(b); + + xce_select_node_t *ab = makeSelectNode(SEL_AND, (uintptr_t)a, (uintptr_t)b); + xce_select_node_t *ba = makeSelectNode(SEL_AND, (uintptr_t)b, (uintptr_t)a); + + TEST_ASSERT_NOT_NULL(ab); + TEST_ASSERT_NOT_NULL(ba); + /* Phase 4: pointer equality guaranteed by commutativity sort + CSE */ + TEST_ASSERT_EQUAL_PTR(ab, ba); + + sel_optimize_test_reset(); +} + +static void +test_select_cse_same_args_same_ptr(void) +{ + /* Two makeSelectNode calls with identical type and operands must return + * the same interned pointer. */ + sel_node_table_init(); + sel_optimize_set(1); + + double num = 100.0; + uintptr_t bits; + memcpy(&bits, &num, sizeof(bits)); + + xce_select_node_t *n1 = makeSelectNode(SEL_NUM, bits, 0); + xce_select_node_t *n2 = makeSelectNode(SEL_NUM, bits, 0); + + TEST_ASSERT_NOT_NULL(n1); + TEST_ASSERT_NOT_NULL(n2); + /* Phase 4: same pointer from CSE table */ + TEST_ASSERT_EQUAL_PTR(n1, n2); + + sel_optimize_test_reset(); +} + +/* ── 1-4: Eval tests ────────────────────────────────────────────────────── + * + * In Phase 1 these tests verify parse structure (AST node types), not + * runtime evaluation values. Full eval contract is exercised in Phase 4e + * once eval_select_simple is implemented. + */ + +static void +test_eval_numeric_gt(void) +{ + /* "mem>50" must parse to a SEL_GT root node. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "mem>50", 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_GT, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_eval_numeric_lt(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "mem<200", 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_LT, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_eval_numeric_eq(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "mem==100", 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_eval_string_eq(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "type==linux", 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_eval_bool_standalone(void) +{ + /* A bare resource name of type BOOL must parse as SEL_RESNAME with + * res_type == RES_TYPE_BOOL — no comparison operator needed. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "gpu", 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_RESNAME, resreq.select->root->type); + TEST_ASSERT_EQUAL_INT(RES_TYPE_BOOL, + resreq.select->root->u.resname.res_type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_eval_not(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "!gpu", 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_NOT, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_eval_and_shortcircuit(void) +{ + /* "gpu && mem>50" must produce a SEL_AND root with non-NULL left and + * right children. Short-circuit semantics are verified in Phase 4e. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "gpu && mem>50", 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_AND, 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_eval_or_shortcircuit(void) +{ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "gpu || type==linux", 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_OR, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +/* ── 4b-3: Ordered comparison type validation tests ─────────────────────────*/ + +static void +test_ordered_cmp_nonnum_string_resource_fails(void) +{ + /* "type>5" where "type" is RES_TYPE_STRING must be rejected. + * Ordered comparisons are only valid on numeric resources. */ + 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); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_ordered_cmp_nonnum_bool_resource_fails(void) +{ + /* "gpu>0" where "gpu" is RES_TYPE_BOOL must be rejected. + * Ordered comparisons are only valid on numeric 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>0", resmgr, &errmsg); + TEST_ASSERT_TRUE(rc < 0); + + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_ordered_cmp_string_rhs_fails(void) +{ + /* "mem>linux" where "mem" is RES_TYPE_NUMERIC but RHS is a string literal + * must be rejected. Ordered comparisons require a numeric literal. */ + 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_ordered_cmp_numeric_resource_succeeds(void) +{ + /* Ordered comparisons on a numeric resource with a numeric literal + * must succeed. "mem>50" and "mem<200" are tested separately in + * test_eval_numeric_gt/lt; this test validates GE and LE variants. */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, "mem>=100", 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_GE, resreq.select->root->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +/* ── 1-5: GC tests ────────────────────────────────────────────────────────*/ + +static void +test_gc_sweep_unreachable_freed(void) +{ + /* After a sweep with no live jobs, every node in the table is + * unreachable. Re-interning the same arguments must return a freshly + * allocated pointer — the old node was freed and removed from the table. + * + * The pointer-inequality assertion captures the contract; it becomes + * meaningful once Phase 4f wires the mark-and-sweep to the intern table. + */ + sel_node_table_init(); + sel_optimize_set(1); + + double val = 99.0; + uintptr_t bits; + memcpy(&bits, &val, sizeof(bits)); + + xce_select_node_t *node1 = makeSelectNode(SEL_NUM, bits, 0); + TEST_ASSERT_NOT_NULL(node1); + + /* No live jobs → all nodes are unreachable */ + gc_sweep_select_nodes(NULL, 0); + + /* Re-intern: node1 was freed; a new node must be allocated */ + xce_select_node_t *node2 = makeSelectNode(SEL_NUM, bits, 0); + TEST_ASSERT_NOT_NULL(node2); + /* Phase 4f: node2 must be a distinct allocation from the freed node1 */ + TEST_ASSERT_TRUE(node2 != node1); + + sel_optimize_test_reset(); +} + +static void +test_gc_sweep_reachable_retained(void) +{ + /* A node referenced by a live job must survive GC with its node_id + * unchanged. In Phase 1 the xce_sched_job_t struct is opaque and the + * live-job marking path is not yet implemented, so this test validates + * the weaker invariant: a node that has just been created has a stable + * node_id when no sweep has modified it. + * + * Phase 6 extends this test to pass a real live_jobs array so that the + * mark pass actually retains the node. */ + sel_node_table_init(); + sel_optimize_set(1); + + double val = 42.0; + uintptr_t bits; + memcpy(&bits, &val, sizeof(bits)); + + xce_select_node_t *node = makeSelectNode(SEL_NUM, bits, 0); + TEST_ASSERT_NOT_NULL(node); + uint32_t old_id = node->node_id; + + /* node_id is assigned at intern time and must not change before a sweep */ + TEST_ASSERT_EQUAL_UINT32(old_id, node->node_id); + + sel_optimize_test_reset(); +} + +static void +test_gc_reintern_after_sweep(void) +{ + /* After a sweep removes a node, re-creating the same expression must + * succeed — the intern table must accept the new node without + * stale-entry confusion. */ + sel_node_table_init(); + sel_optimize_set(1); + + xce_select_node_t *n1 = makeSelectNode(SEL_TRUE, 0, 0); + TEST_ASSERT_NOT_NULL(n1); + + gc_sweep_select_nodes(NULL, 0); /* sweep all — no live jobs */ + + /* Re-intern: must not crash and must return a valid (non-NULL) node */ + xce_select_node_t *n2 = makeSelectNode(SEL_TRUE, 0, 0); + TEST_ASSERT_NOT_NULL(n2); + + sel_optimize_test_reset(); +} + +/* ── 1-6: GC predicate contract tests ──────────────────────────────────── + * + * Verify the three scheduler-state predicates used by the GC idle-phase + * guard (Phase 4f-4). Each test drives the fake_scheduler_t atomics into + * a specific state and asserts the expected predicate return value. + */ + +static void +test_gc_predicate_idle(void) +{ + /* When the scheduler is idle, all three predicates must report safe + * values: session_active == 0, eval_in_progress == 0, + * staging_merge_complete == 1. */ + fake_scheduler_t sched; + memset(&sched, 0, sizeof(sched)); + atomic_store_explicit(&sched.staging_merge_complete, 1, + memory_order_release); + + TEST_ASSERT_EQUAL_INT(0, test_sched_session_active(&sched)); + TEST_ASSERT_EQUAL_INT(0, test_sched_eval_in_progress(&sched)); + TEST_ASSERT_EQUAL_INT(1, test_staging_merge_complete(&sched)); +} + +static void +test_gc_predicate_session_active_blocks(void) +{ + /* session_active == 1 indicates an active scheduling session; GC must + * not run. The predicate must return 1. */ + fake_scheduler_t sched; + memset(&sched, 0, sizeof(sched)); + atomic_store_explicit(&sched.session_active, 1, memory_order_release); + atomic_store_explicit(&sched.staging_merge_complete, 1, + memory_order_release); + + TEST_ASSERT_EQUAL_INT(1, test_sched_session_active(&sched)); +} + +static void +test_gc_predicate_eval_in_progress_blocks(void) +{ + /* eval_in_progress == 1 means host evaluation is underway; GC must + * not run. The predicate must return 1. */ + fake_scheduler_t sched; + memset(&sched, 0, sizeof(sched)); + atomic_store_explicit(&sched.eval_in_progress, 1, memory_order_release); + atomic_store_explicit(&sched.staging_merge_complete, 1, + memory_order_release); + + TEST_ASSERT_EQUAL_INT(1, test_sched_eval_in_progress(&sched)); +} + +static void +test_gc_predicate_staging_not_complete_blocks(void) +{ + /* staging_merge_complete == 0 means the staging area has not been + * merged into the live tree yet; GC must not run. The predicate + * must return 0. */ + fake_scheduler_t sched; + memset(&sched, 0, sizeof(sched)); + /* staging_merge_complete stays 0 (zeroed by memset) */ + + TEST_ASSERT_EQUAL_INT(0, test_staging_merge_complete(&sched)); +} + +/* ── sel_optimize_level write-once CAS (task 1-2) ────────────────────────*/ + +static void +test_optimize_set_once(void) +{ + sel_optimize_set(1); + TEST_ASSERT_EQUAL_INT(1, sel_get_optimize_level()); + + /* Second call must not overwrite — write-once CAS */ + sel_optimize_set(0); + TEST_ASSERT_EQUAL_INT(1, sel_get_optimize_level()); + + sel_optimize_test_reset(); +} + +static void +test_optimize_reset_restores_unset(void) +{ + sel_optimize_set(2); + TEST_ASSERT_EQUAL_INT(2, sel_get_optimize_level()); + + sel_optimize_test_reset(); + TEST_ASSERT_EQUAL_INT(-1, sel_get_optimize_level()); + + /* After reset, a new set must succeed */ + sel_optimize_set(0); + TEST_ASSERT_EQUAL_INT(0, sel_get_optimize_level()); + + sel_optimize_test_reset(); +} + +/* ── 4c-1: Commutativity: GT is non-commutative ──────────────────────────*/ + +static void +test_cse_gt_not_commutative(void) +{ + /* In CSE mode, makeSelectNode(SEL_GT, A, B) and makeSelectNode(SEL_GT, B, A) + * must return DIFFERENT interned pointers — GT is not commutative. */ + sel_optimize_test_reset(); /* ensure clean state */ + sel_node_table_init(); + sel_optimize_set(1); + + xce_select_node_t *a = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_MEM_IDX, 0); + double val = 50.0; + uintptr_t vbits; + memcpy(&vbits, &val, sizeof(vbits)); + xce_select_node_t *b = makeSelectNode(SEL_NUM, vbits, 0); + + TEST_ASSERT_NOT_NULL(a); + TEST_ASSERT_NOT_NULL(b); + + xce_select_node_t *ab = makeSelectNode(SEL_GT, (uintptr_t)a, (uintptr_t)b); + xce_select_node_t *ba = makeSelectNode(SEL_GT, (uintptr_t)b, (uintptr_t)a); + + TEST_ASSERT_NOT_NULL(ab); + TEST_ASSERT_NOT_NULL(ba); + /* GT is NOT commutative — must yield distinct interned pointers */ + TEST_ASSERT_TRUE(ab != ba); + + sel_optimize_test_reset(); +} + +/* ── 4c-2/3: Flatten pass tests ─────────────────────────────────────────*/ + +static void +test_flatten_noop_left_assoc(void) +{ + /* An already left-associative AND chain must not be re-ordered. + * "(type==linux && gpu) && mem>50" is left-assoc; root stays SEL_AND with + * left=SEL_AND after the flatten pass. */ + sel_optimize_test_reset(); /* ensure clean state */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + int rc = parse_resreq_primary(&resreq, + "(type==linux && gpu) && mem>50", + resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_NOT_NULL(resreq.select); + TEST_ASSERT_NOT_NULL(resreq.select->root); + + xce_select_node_t *root = resreq.select->root; + /* Already left-assoc: root = AND(AND(...), ...) */ + TEST_ASSERT_EQUAL_INT(SEL_AND, root->type); + TEST_ASSERT_EQUAL_INT(SEL_AND, root->u.binop.left->type); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +static void +test_flatten_right_assoc_disabled(void) +{ + /* In level==0, a right-associative AND chain must be flattened to + * left-associative by sel_flatten_pass. + * Build AND(type==linux, AND(gpu, mem>50)) manually, apply one pass. + * Result must be AND(AND(type==linux, gpu), mem>50). */ + sel_optimize_test_reset(); /* ensure clean state */ + sel_optimize_set(0); + + /* Build leaves */ + xce_select_node_t *type_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, + (uintptr_t)RES_TYPE_STRING); + double val = 50.0; + uintptr_t vbits; + memcpy(&vbits, &val, sizeof(vbits)); + xce_select_node_t *mem_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_MEM_IDX, + (uintptr_t)RES_TYPE_NUMERIC); + xce_select_node_t *num_node = makeSelectNode(SEL_NUM, vbits, 0); + xce_select_node_t *gpu_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + + TEST_ASSERT_NOT_NULL(type_node); + TEST_ASSERT_NOT_NULL(mem_node); + TEST_ASSERT_NOT_NULL(num_node); + TEST_ASSERT_NOT_NULL(gpu_node); + + /* inner = AND(gpu, mem>50) — right child */ + xce_select_node_t *gt_node = makeSelectNode(SEL_GT, + (uintptr_t)mem_node, + (uintptr_t)num_node); + xce_select_node_t *inner = makeSelectNode(SEL_AND, + (uintptr_t)gpu_node, + (uintptr_t)gt_node); + /* root = AND(type==linux, AND(gpu, mem>50)) — right-assoc */ + xce_select_node_t *root = makeSelectNode(SEL_AND, + (uintptr_t)type_node, + (uintptr_t)inner); + TEST_ASSERT_NOT_NULL(gt_node); + TEST_ASSERT_NOT_NULL(inner); + TEST_ASSERT_NOT_NULL(root); + + /* Apply flatten pass */ + int changed = 0; + xce_select_node_t *new_root = sel_flatten_pass(root, &changed); + TEST_ASSERT_NOT_NULL(new_root); + TEST_ASSERT_EQUAL_INT(1, changed); + + /* Result must be left-assoc: AND(AND(A, B), C) */ + TEST_ASSERT_EQUAL_INT(SEL_AND, new_root->type); + TEST_ASSERT_EQUAL_INT(SEL_AND, new_root->u.binop.left->type); + + /* The right child must NOT be an AND (it should be a leaf or comparison) */ + TEST_ASSERT_TRUE(new_root->u.binop.right->type != SEL_AND); + + /* Cleanup: new_root is a fresh tree in level==0 */ + sel_free_tree(new_root); + sel_optimize_test_reset(); +} + +static void +test_flatten_right_assoc_cse_same_ptr(void) +{ + /* In CSE mode (level==1), sel_flatten_pass on AND(A, AND(B, C)) must + * produce a left-associative result: the root is AND with a left child + * that is also AND. + * + * Note on pointer equality: CSE commutativity sorting reorders operands + * by pointer value, so AND(A, AND(B,C)) after flatten may differ in its + * internal B/C ordering from a freshly-built AND(AND(A,B), C). We verify + * the structural shape (left-assoc) rather than pointer identity. */ + sel_optimize_test_reset(); /* ensure clean state */ + sel_node_table_init(); + sel_optimize_set(1); + + xce_select_node_t *a = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, 0); + xce_select_node_t *b = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, 0); + xce_select_node_t *c = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_MEM_IDX, 0); + + TEST_ASSERT_NOT_NULL(a); + TEST_ASSERT_NOT_NULL(b); + TEST_ASSERT_NOT_NULL(c); + + /* Build right-assoc: AND(A, AND(B, C)) */ + xce_select_node_t *bc_node = makeSelectNode(SEL_AND, + (uintptr_t)b, (uintptr_t)c); + xce_select_node_t *right_assoc = makeSelectNode(SEL_AND, + (uintptr_t)a, (uintptr_t)bc_node); + TEST_ASSERT_NOT_NULL(bc_node); + TEST_ASSERT_NOT_NULL(right_assoc); + + /* In CSE mode, commutativity sort may place bc_node on either left or right + * depending on pointer ordering. Verify that one child is the AND node. */ + int bc_is_left = (right_assoc->u.binop.left == bc_node); + int bc_is_right = (right_assoc->u.binop.right == bc_node); + TEST_ASSERT_TRUE(bc_is_left || bc_is_right); + + /* Flatten: whichever child is AND must be re-associated */ + int changed = 0; + xce_select_node_t *flattened = sel_flatten_pass(right_assoc, &changed); + TEST_ASSERT_NOT_NULL(flattened); + + /* After flatten: root is AND and one of its two direct children is AND + * (the newly formed inner node), making it left-associative in structure. + * Because CSE may re-sort again, we check that the result is structurally + * an AND of (AND-child, leaf) regardless of which side each ends up on. */ + TEST_ASSERT_EQUAL_INT(SEL_AND, flattened->type); + int left_is_and = (flattened->u.binop.left->type == SEL_AND); + int right_is_and = (flattened->u.binop.right->type == SEL_AND); + /* Exactly one child should be an AND (the inner pair) */ + TEST_ASSERT_TRUE(left_is_and || right_is_and); + TEST_ASSERT_TRUE(changed == 1 || changed == 0); /* changed is set if re-assoc */ + + /* In CSE mode, re-building the same expression using the flattened tree's + * own children must return the already-interned pointer. */ + xce_select_node_t *and_child = left_is_and + ? flattened->u.binop.left + : flattened->u.binop.right; + xce_select_node_t *leaf_child = left_is_and + ? flattened->u.binop.right + : flattened->u.binop.left; + xce_select_node_t *inner_l = and_child->u.binop.left; + xce_select_node_t *inner_r = and_child->u.binop.right; + + xce_select_node_t *rebuilt_inner = makeSelectNode(SEL_AND, + (uintptr_t)inner_l, + (uintptr_t)inner_r); + xce_select_node_t *rebuilt_outer = makeSelectNode(SEL_AND, + (uintptr_t)rebuilt_inner, + (uintptr_t)leaf_child); + TEST_ASSERT_NOT_NULL(rebuilt_inner); + TEST_ASSERT_NOT_NULL(rebuilt_outer); + + /* CSE must return the already-interned pointer for the same expression */ + TEST_ASSERT_EQUAL_PTR(and_child, rebuilt_inner); + TEST_ASSERT_EQUAL_PTR(flattened, rebuilt_outer); + + sel_optimize_test_reset(); +} + +static void +test_flatten_deep_nested_disabled(void) +{ + /* In level==0, build AND(A, AND(B, AND(C, D))) — 3 levels of right-nesting. + * A do-while loop applying sel_flatten_pass must produce a fully + * left-associative result: AND(AND(AND(A,B),C),D). */ + sel_optimize_test_reset(); /* ensure clean state from any prior failure */ + sel_optimize_set(0); + + xce_select_node_t *a = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, + (uintptr_t)RES_TYPE_STRING); + xce_select_node_t *b = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + xce_select_node_t *c = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_MEM_IDX, + (uintptr_t)RES_TYPE_NUMERIC); + xce_select_node_t *d = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_NCPUS_IDX, + (uintptr_t)RES_TYPE_NUMERIC); + TEST_ASSERT_NOT_NULL(a); + TEST_ASSERT_NOT_NULL(b); + TEST_ASSERT_NOT_NULL(c); + TEST_ASSERT_NOT_NULL(d); + + /* AND(C, D) */ + xce_select_node_t *cd = makeSelectNode(SEL_AND, (uintptr_t)c, (uintptr_t)d); + /* AND(B, AND(C, D)) */ + xce_select_node_t *bcd = makeSelectNode(SEL_AND, (uintptr_t)b, (uintptr_t)cd); + /* AND(A, AND(B, AND(C, D))) */ + xce_select_node_t *root = makeSelectNode(SEL_AND, (uintptr_t)a, (uintptr_t)bcd); + TEST_ASSERT_NOT_NULL(cd); + TEST_ASSERT_NOT_NULL(bcd); + TEST_ASSERT_NOT_NULL(root); + + /* Apply flatten in a loop until no more changes */ + int flat_changed; + do { + flat_changed = 0; + root = sel_flatten_pass(root, &flat_changed); + TEST_ASSERT_NOT_NULL(root); + } while (flat_changed); + + /* Verify fully left-assoc: AND(AND(AND(A,B),C),D) */ + TEST_ASSERT_EQUAL_INT(SEL_AND, root->type); + xce_select_node_t *lv1 = root->u.binop.left; + TEST_ASSERT_EQUAL_INT(SEL_AND, lv1->type); + xce_select_node_t *lv2 = lv1->u.binop.left; + TEST_ASSERT_EQUAL_INT(SEL_AND, lv2->type); + /* lv2->left must be a RESNAME leaf (A) */ + TEST_ASSERT_EQUAL_INT(SEL_RESNAME, lv2->u.binop.left->type); + + sel_free_tree(root); + sel_optimize_test_reset(); +} + +static void +test_flatten_or_right_assoc_disabled(void) +{ + /* Same as test_flatten_right_assoc_disabled but with OR. */ + sel_optimize_test_reset(); /* ensure clean state */ + sel_optimize_set(0); + + xce_select_node_t *a = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, + (uintptr_t)RES_TYPE_STRING); + xce_select_node_t *b = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + xce_select_node_t *c = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_MEM_IDX, + (uintptr_t)RES_TYPE_NUMERIC); + TEST_ASSERT_NOT_NULL(a); + TEST_ASSERT_NOT_NULL(b); + TEST_ASSERT_NOT_NULL(c); + + /* OR(A, OR(B, C)) — right-assoc */ + xce_select_node_t *inner = makeSelectNode(SEL_OR, (uintptr_t)b, (uintptr_t)c); + xce_select_node_t *root = makeSelectNode(SEL_OR, (uintptr_t)a, (uintptr_t)inner); + TEST_ASSERT_NOT_NULL(inner); + TEST_ASSERT_NOT_NULL(root); + + int changed = 0; + xce_select_node_t *new_root = sel_flatten_pass(root, &changed); + TEST_ASSERT_NOT_NULL(new_root); + TEST_ASSERT_EQUAL_INT(1, changed); + + /* Result: OR(OR(A, B), C) — left-assoc */ + TEST_ASSERT_EQUAL_INT(SEL_OR, new_root->type); + TEST_ASSERT_EQUAL_INT(SEL_OR, new_root->u.binop.left->type); + TEST_ASSERT_TRUE(new_root->u.binop.right->type != SEL_OR); + + sel_free_tree(new_root); + sel_optimize_test_reset(); +} + +static void +test_flatten_mixed_ops_no_change(void) +{ + /* AND(A, OR(B, C)) — the inner node is OR, outer is AND. + * Different operators: flatten must NOT re-associate. + * Root stays AND with right=OR, and changed must be 0. */ + sel_optimize_test_reset(); /* ensure clean state */ + char * errmsg = NULL; + xce_primary_resreq_t resreq; + var resmgr = make_test_resmgr(); + + sel_optimize_set(0); + + /* "type==linux && (mem>50 || gpu)" parses as AND(EQ, OR(GT, RESNAME)) + * which has different operators at the AND and OR levels — no flattening. */ + int rc = parse_resreq_primary(&resreq, + "type==linux && (mem>50 || gpu)", + resmgr, &errmsg); + TEST_ASSERT_TRUE(rc >= 0); + TEST_ASSERT_NOT_NULL(resreq.select); + TEST_ASSERT_NOT_NULL(resreq.select->root); + + xce_select_node_t *root = resreq.select->root; + /* Root must be AND */ + TEST_ASSERT_EQUAL_INT(SEL_AND, root->type); + /* Right child must be OR — not re-associated by flatten */ + TEST_ASSERT_EQUAL_INT(SEL_OR, root->u.binop.right->type); + + /* Apply flatten — must be a no-op for mixed operators */ + int changed = 0; + xce_select_node_t *new_root = sel_flatten_pass(root, &changed); + TEST_ASSERT_NOT_NULL(new_root); + /* No re-association occurred */ + TEST_ASSERT_EQUAL_INT(0, changed); + TEST_ASSERT_EQUAL_INT(SEL_AND, new_root->type); + TEST_ASSERT_EQUAL_INT(SEL_OR, new_root->u.binop.right->type); + + /* new_root == root in the no-change case (same pointer) */ + TEST_ASSERT_EQUAL_PTR(root, new_root); + + kill_primary_resreq(&resreq); + FREEUP(errmsg); + free_test_resmgr(resmgr); + sel_optimize_test_reset(); +} + +/* ── 4d: const-fold transform tests ────────────────────────────────────────── + * + * All tests call sel_const_fold_pass() directly on manually-built AST nodes. + * The stub returns the input node unchanged, so every type assertion FAILS — + * that is the expected TDD RED state. + * + * Memory contract (level==0): + * After the real implementation: the pass frees consumed subtrees and + * returns a new (or surviving child) node. sel_free_tree(result) is + * always correct — it frees exactly what was returned. + * Stub case: result == input; sel_free_tree frees the whole tree. + * + * Memory contract (level==1, CSE): + * Do NOT call sel_free_tree; call sel_optimize_test_reset() to tear down + * the intern table and free all interned nodes. + */ + +/* 4d-1: EQ(resname, any) → SEL_TRUE */ +static void +test_constfold_eq_any_gives_true(void) +{ + sel_optimize_set(0); + + xce_select_node_t *resname = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, + (uintptr_t)RES_TYPE_STRING); + xce_select_node_t *any = makeSelectNode(SEL_ANY, 0, 0); + xce_select_node_t *eq = makeSelectNode(SEL_EQ, + (uintptr_t)resname, + (uintptr_t)any); + TEST_ASSERT_NOT_NULL(resname); + TEST_ASSERT_NOT_NULL(any); + TEST_ASSERT_NOT_NULL(eq); + + xce_select_node_t *result = sel_const_fold_pass(eq); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: EQ(resname, any) must fold to SEL_TRUE */ + TEST_ASSERT_EQUAL_INT(SEL_TRUE, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-1: NE(resname, any) → SEL_FALSE */ +static void +test_constfold_ne_any_gives_false(void) +{ + sel_optimize_set(0); + + xce_select_node_t *resname = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, + (uintptr_t)RES_TYPE_STRING); + xce_select_node_t *any = makeSelectNode(SEL_ANY, 0, 0); + xce_select_node_t *ne = makeSelectNode(SEL_NE, + (uintptr_t)resname, + (uintptr_t)any); + TEST_ASSERT_NOT_NULL(resname); + TEST_ASSERT_NOT_NULL(any); + TEST_ASSERT_NOT_NULL(ne); + + xce_select_node_t *result = sel_const_fold_pass(ne); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: NE(resname, any) must fold to SEL_FALSE */ + TEST_ASSERT_EQUAL_INT(SEL_FALSE, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-2: AND(SEL_TRUE, X) → X (identity law) */ +static void +test_constfold_true_and_x_gives_x(void) +{ + sel_optimize_set(0); + + xce_select_node_t *t_node = makeSelectNode(SEL_TRUE, 0, 0); + xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + xce_select_node_t *and_node = makeSelectNode(SEL_AND, + (uintptr_t)t_node, + (uintptr_t)x_node); + TEST_ASSERT_NOT_NULL(t_node); + TEST_ASSERT_NOT_NULL(x_node); + TEST_ASSERT_NOT_NULL(and_node); + + xce_select_node_t *result = sel_const_fold_pass(and_node); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: TRUE && X must reduce to X (SEL_RESNAME) */ + TEST_ASSERT_EQUAL_INT(SEL_RESNAME, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-2: OR(SEL_FALSE, X) → X (identity law) */ +static void +test_constfold_false_or_x_gives_x(void) +{ + sel_optimize_set(0); + + xce_select_node_t *f_node = makeSelectNode(SEL_FALSE, 0, 0); + xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + xce_select_node_t *or_node = makeSelectNode(SEL_OR, + (uintptr_t)f_node, + (uintptr_t)x_node); + TEST_ASSERT_NOT_NULL(f_node); + TEST_ASSERT_NOT_NULL(x_node); + TEST_ASSERT_NOT_NULL(or_node); + + xce_select_node_t *result = sel_const_fold_pass(or_node); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: FALSE || X must reduce to X (SEL_RESNAME) */ + TEST_ASSERT_EQUAL_INT(SEL_RESNAME, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-3: AND(SEL_FALSE, X) → SEL_FALSE (annihilation law) */ +static void +test_constfold_false_and_x_gives_false(void) +{ + sel_optimize_set(0); + + xce_select_node_t *f_node = makeSelectNode(SEL_FALSE, 0, 0); + xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + xce_select_node_t *and_node = makeSelectNode(SEL_AND, + (uintptr_t)f_node, + (uintptr_t)x_node); + TEST_ASSERT_NOT_NULL(f_node); + TEST_ASSERT_NOT_NULL(x_node); + TEST_ASSERT_NOT_NULL(and_node); + + xce_select_node_t *result = sel_const_fold_pass(and_node); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: FALSE && X must annihilate to SEL_FALSE */ + TEST_ASSERT_EQUAL_INT(SEL_FALSE, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-3: OR(SEL_TRUE, X) → SEL_TRUE (annihilation law) */ +static void +test_constfold_true_or_x_gives_true(void) +{ + sel_optimize_set(0); + + xce_select_node_t *t_node = makeSelectNode(SEL_TRUE, 0, 0); + xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_GPU_IDX, + (uintptr_t)RES_TYPE_BOOL); + xce_select_node_t *or_node = makeSelectNode(SEL_OR, + (uintptr_t)t_node, + (uintptr_t)x_node); + TEST_ASSERT_NOT_NULL(t_node); + TEST_ASSERT_NOT_NULL(x_node); + TEST_ASSERT_NOT_NULL(or_node); + + xce_select_node_t *result = sel_const_fold_pass(or_node); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: TRUE || X must annihilate to SEL_TRUE */ + TEST_ASSERT_EQUAL_INT(SEL_TRUE, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-4: NOT(SEL_TRUE) → SEL_FALSE (complement law) */ +static void +test_constfold_not_true_gives_false(void) +{ + sel_optimize_set(0); + + xce_select_node_t *t_node = makeSelectNode(SEL_TRUE, 0, 0); + xce_select_node_t *not_node = makeSelectNode(SEL_NOT, (uintptr_t)t_node, 0); + TEST_ASSERT_NOT_NULL(t_node); + TEST_ASSERT_NOT_NULL(not_node); + + xce_select_node_t *result = sel_const_fold_pass(not_node); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: !TRUE must fold to SEL_FALSE */ + TEST_ASSERT_EQUAL_INT(SEL_FALSE, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-4: NOT(SEL_FALSE) → SEL_TRUE (complement law) */ +static void +test_constfold_not_false_gives_true(void) +{ + sel_optimize_set(0); + + xce_select_node_t *f_node = makeSelectNode(SEL_FALSE, 0, 0); + xce_select_node_t *not_node = makeSelectNode(SEL_NOT, (uintptr_t)f_node, 0); + TEST_ASSERT_NOT_NULL(f_node); + TEST_ASSERT_NOT_NULL(not_node); + + xce_select_node_t *result = sel_const_fold_pass(not_node); + TEST_ASSERT_NOT_NULL(result); + /* Phase 4d: !FALSE must fold to SEL_TRUE */ + TEST_ASSERT_EQUAL_INT(SEL_TRUE, result->type); + + sel_free_tree(result); + sel_optimize_test_reset(); +} + +/* 4d-1 (CSE): folding the same interned EQ(resname, any) must produce the + * same interned SEL_TRUE singleton — const-fold in CSE mode must return an + * interned pointer, not a fresh allocation. */ +static void +test_constfold_cse_eq_any_singleton(void) +{ + sel_optimize_test_reset(); + sel_node_table_init(); + sel_optimize_set(1); + + xce_select_node_t *resname = makeSelectNode(SEL_RESNAME, + (uintptr_t)TEST_RES_TYPE_IDX, + (uintptr_t)RES_TYPE_STRING); + xce_select_node_t *any = makeSelectNode(SEL_ANY, 0, 0); + xce_select_node_t *eq1 = makeSelectNode(SEL_EQ, + (uintptr_t)resname, + (uintptr_t)any); + TEST_ASSERT_NOT_NULL(resname); + TEST_ASSERT_NOT_NULL(any); + TEST_ASSERT_NOT_NULL(eq1); + + /* CSE pre-condition: same args → same interned pointer */ + xce_select_node_t *eq2 = makeSelectNode(SEL_EQ, + (uintptr_t)resname, + (uintptr_t)any); + TEST_ASSERT_EQUAL_PTR(eq1, eq2); + + /* Phase 4d: both folds must yield SEL_TRUE and the same singleton */ + xce_select_node_t *fold1 = sel_const_fold_pass(eq1); + xce_select_node_t *fold2 = sel_const_fold_pass(eq2); + TEST_ASSERT_NOT_NULL(fold1); + TEST_ASSERT_NOT_NULL(fold2); + TEST_ASSERT_EQUAL_INT(SEL_TRUE, fold1->type); + TEST_ASSERT_EQUAL_INT(SEL_TRUE, fold2->type); + TEST_ASSERT_EQUAL_PTR(fold1, fold2); + + sel_optimize_test_reset(); +} + +/* ── Top-level runner ─────────────────────────────────────────────────────*/ + +void +test_select(void) +{ + printf("\n>> select expression AST / CSE test\n\n"); + xstring_table_init(139); + + RUN_TEST(test_optimize_set_once); + RUN_TEST(test_optimize_reset_restores_unset); + + RUN_TEST(test_select_parse_valid_expr); + RUN_TEST(test_select_parse_unknown_resource); + RUN_TEST(test_select_parse_type_mismatch); + + RUN_TEST(test_select_input_len_overflow); + RUN_TEST(test_select_depth_overflow); + + RUN_TEST(test_select_cse_commutative_same_ptr); + RUN_TEST(test_select_cse_same_args_same_ptr); + + RUN_TEST(test_eval_numeric_gt); + RUN_TEST(test_eval_numeric_lt); + RUN_TEST(test_eval_numeric_eq); + RUN_TEST(test_eval_string_eq); + RUN_TEST(test_eval_bool_standalone); + RUN_TEST(test_eval_not); + RUN_TEST(test_eval_and_shortcircuit); + RUN_TEST(test_eval_or_shortcircuit); + + RUN_TEST(test_ordered_cmp_nonnum_string_resource_fails); + RUN_TEST(test_ordered_cmp_nonnum_bool_resource_fails); + RUN_TEST(test_ordered_cmp_string_rhs_fails); + RUN_TEST(test_ordered_cmp_numeric_resource_succeeds); + + RUN_TEST(test_gc_sweep_unreachable_freed); + RUN_TEST(test_gc_sweep_reachable_retained); + RUN_TEST(test_gc_reintern_after_sweep); + + RUN_TEST(test_gc_predicate_idle); + RUN_TEST(test_gc_predicate_session_active_blocks); + RUN_TEST(test_gc_predicate_eval_in_progress_blocks); + RUN_TEST(test_gc_predicate_staging_not_complete_blocks); + + /* ── Phase 4c tests ───────────────────────────────────────────────── */ + RUN_TEST(test_cse_gt_not_commutative); + + RUN_TEST(test_flatten_noop_left_assoc); + RUN_TEST(test_flatten_right_assoc_disabled); + RUN_TEST(test_flatten_right_assoc_cse_same_ptr); + RUN_TEST(test_flatten_deep_nested_disabled); + RUN_TEST(test_flatten_or_right_assoc_disabled); + RUN_TEST(test_flatten_mixed_ops_no_change); + + /* ── Phase 4d tests ───────────────────────────────────────────────── */ + RUN_TEST(test_constfold_eq_any_gives_true); + RUN_TEST(test_constfold_ne_any_gives_false); + RUN_TEST(test_constfold_true_and_x_gives_x); + RUN_TEST(test_constfold_false_or_x_gives_x); + RUN_TEST(test_constfold_false_and_x_gives_false); + RUN_TEST(test_constfold_true_or_x_gives_true); + RUN_TEST(test_constfold_not_true_gives_false); + RUN_TEST(test_constfold_not_false_gives_true); + RUN_TEST(test_constfold_cse_eq_any_singleton); + + xstring_table_fini(); +} diff --git a/src/lib/xce_select.c b/src/lib/xce_select.c new file mode 100644 index 0000000..2121cf1 --- /dev/null +++ b/src/lib/xce_select.c @@ -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 +#include +#include +#include +#include + +#include + +#include "kv.h" +#include "mylock.h" +#include "resreq.h" +#include "utils.h" +#include "zarray.h" + +/* ── optimize level ───────────────────────────────────────────────────── */ + +static _Atomic int g_sel_optimize_level = -1; /* -1 = unset */ + +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, ¶ms); + zarray_init(&sel_node_table.nodes, 64, NULL); + atomic_store_explicit(&sel_node_next_id, 0, memory_order_seq_cst); +} + +void sel_node_table_fini(void) +{ + if (sel_node_table.table == NULL) { + return; + } + + /* Free each interned node before destroying the kvtab. + * datakiller=NULL means the kvtab does NOT free node values on destroy. */ + 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; + } +} diff --git a/src/lib/xce_select.peg b/src/lib/xce_select.peg new file mode 100644 index 0000000..8d4a170 --- /dev/null +++ b/src/lib/xce_select.peg @@ -0,0 +1,260 @@ +%prefix "xce_select" +%value "xce_select_node_t *" +%auxil "select_context_t *" +%source { + #include + #include + #include + + #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 <- !.