feat: implement Phase 4d const-fold transform for select AST
Adds sel_const_fold_pass() — a single-pass bottom-up const-fold that
runs after the Phase 4c flatten loop in parse_select():
4d-1 EQ/NE with either operand SEL_ANY → SEL_TRUE / SEL_FALSE
(checks both child slots — commutativity sort makes SEL_ANY
position non-deterministic after makeSelectNode interning)
4d-2 Identity laws: TRUE&&X→X, X&&TRUE→X, FALSE||X→X, X||FALSE→X
4d-3 Annihilation laws: FALSE&&X→FALSE, TRUE||X→TRUE (symmetric)
4d-4 Complement laws: !TRUE→FALSE, !FALSE→TRUE
Memory semantics:
level==0 (API server): frees orphaned nodes via free_select_node /
sel_free_tree; OOM from makeSelectNode returns original node safely
level>0 (CSE/scheduler): never manually frees; annihilation folds
return the already-interned left/right singleton directly (no
redundant makeSelectNode call); OOM guard unconditional so CSE
mode also degrades gracefully instead of returning NULL
9 new TDD tests added to test_select.c (RED→GREEN verified).
All 166 tests pass (157 prior + 9 Phase 4d).
This commit is contained in:
parent
c2ad96a5e0
commit
b38babb7d6
9 changed files with 3248 additions and 5 deletions
71
src/docs/p1-2-host-select.md
Normal file
71
src/docs/p1-2-host-select.md
Normal file
|
|
@ -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.
|
||||
220
src/docs/resreq-ast-cse-impl-plan.md
Normal file
220
src/docs/resreq-ast-cse-impl-plan.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -2,14 +2,121 @@
|
|||
|
||||
#define _RESREQ_H_ 1
|
||||
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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*,
|
||||
|
|
|
|||
159
src/lib/resreq.c
159
src/lib/resreq.c
|
|
@ -1,4 +1,7 @@
|
|||
#define _XOPEN_SOURCE 700
|
||||
|
||||
#include <errno.h>
|
||||
#include <locale.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -8,10 +11,24 @@
|
|||
#include "rusage.h"
|
||||
#include "section.h"
|
||||
#include "utils.h"
|
||||
#include "xce/types.h"
|
||||
#include "xce_resreq.h"
|
||||
#include "xce_select_peg.h"
|
||||
#include "xlist.h"
|
||||
#include "xvec.h"
|
||||
|
||||
res_type_t valtype_to_res_type(int v)
|
||||
{
|
||||
switch (v) {
|
||||
case XCE_VAL_NUMERIC: return RES_TYPE_NUMERIC;
|
||||
case XCE_VAL_STRING: return RES_TYPE_STRING;
|
||||
case XCE_VAL_BOOLEAN: return RES_TYPE_BOOL;
|
||||
default:
|
||||
ERROR("valtype_to_res_type: unknown valtype %d", v);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
static int get_parse_errmsg(char** errmsg, const char* data, int pos)
|
||||
{
|
||||
size_t bufsize = 0;
|
||||
|
|
@ -153,8 +170,127 @@ int parse_rusage(xlist_t* rusages, char* data, char** errmsg)
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* Depth pre-scan: count '!' and '(' as depth increments.
|
||||
* Returns 0 if depth is within limit, -1 if exceeded. */
|
||||
static int check_select_depth(const char* input, int max_depth)
|
||||
{
|
||||
int depth = 0;
|
||||
const char* p;
|
||||
for (p = input; *p; p++) {
|
||||
if (*p == '!' || *p == '(') {
|
||||
depth++;
|
||||
if (depth > max_depth) return -1;
|
||||
} else if (*p == ')') {
|
||||
if (depth > 0) depth--;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_select(xce_primary_resreq_t* primary, char* selectString,
|
||||
var resmgr, char** errmsg)
|
||||
{
|
||||
select_context_t ctx;
|
||||
xce_select_context_t* p = NULL;
|
||||
int rc = 1;
|
||||
|
||||
/* Without a resource manager, resource names cannot be resolved.
|
||||
* Leave primary->select = NULL and succeed — callers that need
|
||||
* select evaluation must supply a valid resmgr. */
|
||||
if (resmgr == NULL) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
|
||||
/* Length guard */
|
||||
if (strlen(selectString) > SELECT_MAX_INPUT_LEN) {
|
||||
*errmsg = strdup("select expression exceeds maximum length");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Depth guard */
|
||||
if (check_select_depth(selectString, SELECT_MAX_DEPTH) < 0) {
|
||||
*errmsg = strdup("select expression exceeds maximum nesting depth");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
ctx.resmgr = resmgr;
|
||||
ctx.input = selectString;
|
||||
|
||||
p = xce_select_create(&ctx);
|
||||
if (p == NULL) return -errno;
|
||||
|
||||
/* LC_NUMERIC guard: strtod/snprintf in PEG actions must use '.' as the
|
||||
* decimal separator regardless of the calling thread's locale setting. */
|
||||
locale_t cloc = newlocale(LC_NUMERIC_MASK, "C", (locale_t)0);
|
||||
if (cloc == (locale_t)0) {
|
||||
xce_select_destroy(p);
|
||||
if (sel_get_optimize_level() > 0) {
|
||||
ERROR("newlocale(LC_NUMERIC_MASK,C) failed: %s — scheduler cannot "
|
||||
"safely parse numeric resreq values", strerror(errno));
|
||||
abort();
|
||||
}
|
||||
ERROR("newlocale(LC_NUMERIC_MASK,C) failed: %s — rejecting resreq parse",
|
||||
strerror(errno));
|
||||
*errmsg = strdup("locale initialization failed");
|
||||
return -EINVAL;
|
||||
}
|
||||
locale_t old = uselocale(cloc);
|
||||
|
||||
xce_select_parse(p, NULL);
|
||||
|
||||
uselocale(old);
|
||||
freelocale(cloc);
|
||||
|
||||
if (ctx.has_error || ctx.root == NULL) {
|
||||
if (ctx.errmsg[0] != '\0')
|
||||
*errmsg = strdup(ctx.errmsg);
|
||||
else
|
||||
get_parse_errmsg(errmsg, selectString, ctx.err_pos);
|
||||
rc = -EINVAL;
|
||||
goto cleanup_parser;
|
||||
}
|
||||
|
||||
/* Phase 4c: flatten AND/OR right-associative chains to left-associative */
|
||||
{
|
||||
int flat_changed;
|
||||
do {
|
||||
flat_changed = 0;
|
||||
ctx.root = sel_flatten_pass(ctx.root, &flat_changed);
|
||||
if (ctx.root == NULL) {
|
||||
rc = -ENOMEM;
|
||||
goto cleanup_parser;
|
||||
}
|
||||
} while (flat_changed);
|
||||
}
|
||||
|
||||
/* Phase 4d: const-fold (single pass — does not create new flatten opportunities) */
|
||||
ctx.root = sel_const_fold_pass(ctx.root);
|
||||
if (ctx.root == NULL) {
|
||||
rc = -ENOMEM;
|
||||
goto cleanup_parser;
|
||||
}
|
||||
|
||||
{
|
||||
xce_select_t* sel = calloc(1, sizeof(*sel));
|
||||
if (sel == NULL) {
|
||||
if (sel_get_optimize_level() == 0)
|
||||
sel_free_tree(ctx.root);
|
||||
rc = -ENOMEM;
|
||||
} else {
|
||||
sel->root = ctx.root;
|
||||
primary->select = sel;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_parser:
|
||||
xce_select_destroy(p);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int parse_resreq_primary(xce_primary_resreq_t* primary, char* resreqString,
|
||||
char** errmsg)
|
||||
var resmgr, char** errmsg)
|
||||
{
|
||||
int rc;
|
||||
xce_resreq_data_t data;
|
||||
|
|
@ -170,6 +306,15 @@ int parse_resreq_primary(xce_primary_resreq_t* primary, char* resreqString,
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* Parse select clause BEFORE kill_resreq_data (data.selectString freed there) */
|
||||
if (data.selectString != NULL && data.selectString[0] != '\0') {
|
||||
rc = parse_select(primary, data.selectString, resmgr, errmsg);
|
||||
if (rc < 0) {
|
||||
kill_resreq_data(&data);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
xlist_init(&(primary->rusages), 8);
|
||||
|
||||
if ((data.rusageString != NULL) && (strlen(data.rusageString) != 0)) {
|
||||
|
|
@ -197,7 +342,7 @@ void kill_resreq_ctx(resreq_context_t* ctx)
|
|||
}
|
||||
}
|
||||
|
||||
int parse_resreq(xce_resreq_t* resreq, char* data, char** errmsg)
|
||||
int parse_resreq(xce_resreq_t* resreq, char* data, var resmgr, char** errmsg)
|
||||
{
|
||||
resreq_context_t ctx = { 0 };
|
||||
xce_resreq_context_t* p = NULL;
|
||||
|
|
@ -210,6 +355,7 @@ int parse_resreq(xce_resreq_t* resreq, char* data, char** errmsg)
|
|||
*errmsg = NULL;
|
||||
memset(resreq, 0x0, sizeof(*resreq));
|
||||
|
||||
ctx.resmgr = resmgr;
|
||||
ctx.input = data;
|
||||
ctx.resreq = resreq;
|
||||
|
||||
|
|
@ -241,6 +387,15 @@ void kill_primary_resreq(xce_primary_resreq_t* resreq)
|
|||
return;
|
||||
}
|
||||
|
||||
if (resreq->select != NULL) {
|
||||
xce_select_node_t* root = resreq->select->root;
|
||||
resreq->select->root = NULL;
|
||||
if (sel_get_optimize_level() == 0 && root != NULL)
|
||||
sel_free_tree(root);
|
||||
FREEUP(resreq->select);
|
||||
}
|
||||
resreq->select = NULL;
|
||||
|
||||
for (i = 0; i < resreq->rusages.count; i++) {
|
||||
rusage_value_t* v = (rusage_value_t*)xlist_get(&(resreq->rusages), i);
|
||||
|
||||
|
|
|
|||
58
src/lib/testsuite/test_resmgr_fixture.c
Normal file
58
src/lib/testsuite/test_resmgr_fixture.c
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <rtti.h>
|
||||
|
||||
#include "kv.h"
|
||||
#include "resreq.h"
|
||||
#include "xce/types.h"
|
||||
#include "test_resmgr_fixture.h"
|
||||
|
||||
/* Internal helper to add one resource to the test resmgr */
|
||||
static void add_resource(var resmgr, const char* name, int idx, int valtype)
|
||||
{
|
||||
xce_resource_t* r = calloc(1, sizeof(*r));
|
||||
if (r == NULL) return;
|
||||
r->name = strdup(name);
|
||||
r->idx = idx;
|
||||
r->valtype = valtype;
|
||||
key_add(resmgr, (void*)r->name, r);
|
||||
}
|
||||
|
||||
var make_test_resmgr(void)
|
||||
{
|
||||
kv_params_t params = {
|
||||
.nslots = 64,
|
||||
.keyops = &string_key_ops,
|
||||
.datakiller = NULL, /* we free resources manually in free_test_resmgr */
|
||||
};
|
||||
var resmgr = makeInstance(&kvtab_type, ¶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);
|
||||
}
|
||||
19
src/lib/testsuite/test_resmgr_fixture.h
Normal file
19
src/lib/testsuite/test_resmgr_fixture.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#if !defined(_TEST_RESMGR_FIXTURE_H_)
|
||||
#define _TEST_RESMGR_FIXTURE_H_
|
||||
|
||||
#include <rtti.h>
|
||||
|
||||
/* Test resource indices — must match make_test_resmgr() registration order */
|
||||
#define TEST_RES_TYPE_IDX 0 /* "type" — RES_TYPE_STRING */
|
||||
#define TEST_RES_MEM_IDX 1 /* "mem" — RES_TYPE_NUMERIC */
|
||||
#define TEST_RES_GPU_IDX 2 /* "gpu" — RES_TYPE_BOOL */
|
||||
#define TEST_RES_NCPUS_IDX 3 /* "ncpus" — RES_TYPE_NUMERIC */
|
||||
#define TEST_RES_ARCH_IDX 4 /* "arch" — RES_TYPE_STRING */
|
||||
|
||||
/* Create a test resmgr with 5 pre-registered resources.
|
||||
* Returns a var (kvtab) that can be passed to parse_select / parse_resreq_primary.
|
||||
* Caller must call free_test_resmgr() when done. */
|
||||
extern var make_test_resmgr(void);
|
||||
extern void free_test_resmgr(var resmgr);
|
||||
|
||||
#endif
|
||||
1365
src/lib/testsuite/test_select.c
Normal file
1365
src/lib/testsuite/test_select.c
Normal file
File diff suppressed because it is too large
Load diff
956
src/lib/xce_select.c
Normal file
956
src/lib/xce_select.c
Normal file
|
|
@ -0,0 +1,956 @@
|
|||
/* xce_select.c — select-expression AST with CSE intern table (Phase 4a).
|
||||
*
|
||||
* Phase 3: non-CSE path (level 0) — allocate and fill nodes directly.
|
||||
* Phase 4a: CSE intern table for level > 0 — commutativity sort + hash-cons.
|
||||
*/
|
||||
|
||||
#define UNIT_TEST 1 /* expose sel_optimize_test_reset in resreq.h */
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <rtti.h>
|
||||
|
||||
#include "kv.h"
|
||||
#include "mylock.h"
|
||||
#include "resreq.h"
|
||||
#include "utils.h"
|
||||
#include "zarray.h"
|
||||
|
||||
/* ── optimize level ───────────────────────────────────────────────────── */
|
||||
|
||||
static _Atomic int g_sel_optimize_level = -1; /* -1 = unset */
|
||||
|
||||
void sel_optimize_set(int level)
|
||||
{
|
||||
int expected = -1;
|
||||
/* Write-once CAS: only succeeds when still unset (-1). */
|
||||
atomic_compare_exchange_strong_explicit(
|
||||
&g_sel_optimize_level, &expected, level,
|
||||
memory_order_seq_cst, memory_order_seq_cst);
|
||||
}
|
||||
|
||||
int sel_get_optimize_level(void)
|
||||
{
|
||||
return atomic_load_explicit(&g_sel_optimize_level, memory_order_acquire);
|
||||
}
|
||||
|
||||
/* ── CSE intern table ─────────────────────────────────────────────────── */
|
||||
|
||||
/* Key for the intern table — uniquely identifies a select node by its
|
||||
* type and operands. Padding bytes are zero-initialised via the struct
|
||||
* literal so that memcmp-based comparison is correct. */
|
||||
typedef struct {
|
||||
sel_node_type_t type;
|
||||
uintptr_t op1;
|
||||
uintptr_t op2;
|
||||
} sel_node_key_t;
|
||||
|
||||
/* Build a canonical key, applying commutativity sort for AND/OR/EQ/NE. */
|
||||
static sel_node_key_t sel_make_key(sel_node_type_t type,
|
||||
uintptr_t op1, uintptr_t op2)
|
||||
{
|
||||
sel_node_key_t key = { .type = type, .op1 = op1, .op2 = op2 };
|
||||
|
||||
/* Commutative operators: sort operands so A op B == B op A. */
|
||||
switch (type) {
|
||||
case SEL_AND:
|
||||
case SEL_OR:
|
||||
case SEL_EQ:
|
||||
case SEL_NE:
|
||||
if (key.op1 > key.op2) {
|
||||
uintptr_t tmp = key.op1;
|
||||
key.op1 = key.op2;
|
||||
key.op2 = tmp;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/* For RESNAME, res_type (op2) is derived from the resource manager and
|
||||
* res_idx (op1) uniquely identifies the resource — zero op2 in the key. */
|
||||
if (type == SEL_RESNAME) {
|
||||
key.op2 = 0;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/* Key operations for the kv table. */
|
||||
|
||||
static uint32_t sel_key_hash_fn(void* k)
|
||||
{
|
||||
sel_node_key_t* key = (sel_node_key_t*)k;
|
||||
uint64_t h;
|
||||
|
||||
h = hash64((uint64_t)key->type);
|
||||
h ^= hash64(key->op1) * UINT64_C(0x9e3779b97f4a7c15);
|
||||
h ^= hash64(key->op2) * UINT64_C(0x6c62272e07bb0142);
|
||||
return (uint32_t)(h ^ (h >> 32));
|
||||
}
|
||||
|
||||
static int sel_key_cmp_fn(void* a, void* b)
|
||||
{
|
||||
return memcmp(a, b, sizeof(sel_node_key_t));
|
||||
}
|
||||
|
||||
static int sel_key_assign_fn(void** dst, void* src)
|
||||
{
|
||||
sel_node_key_t* copy = (sel_node_key_t*)malloc(sizeof(sel_node_key_t));
|
||||
if (copy == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
memcpy(copy, src, sizeof(sel_node_key_t));
|
||||
*dst = copy;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void sel_key_destroy_fn(void* k) { free(k); }
|
||||
|
||||
static kv_key_ops_t sel_node_key_ops = {
|
||||
.hash = sel_key_hash_fn,
|
||||
.assign = sel_key_assign_fn,
|
||||
.cmp = sel_key_cmp_fn,
|
||||
.destroy = sel_key_destroy_fn,
|
||||
};
|
||||
|
||||
/* Hard cap on the intern table size — prevents unbounded memory growth. */
|
||||
#define SEL_MAX_INTERNED_NODES 1000000
|
||||
|
||||
/* Node ID counter — atomically incremented for each new interned node. */
|
||||
static _Atomic uint32_t sel_node_next_id = 0;
|
||||
|
||||
/* Global intern table state. */
|
||||
static struct {
|
||||
mylock_t lock;
|
||||
var table; /* kvtab instance: key=sel_node_key_t*, value=node* */
|
||||
zarray_t nodes; /* all live interned nodes, for GC sweep */
|
||||
} sel_node_table;
|
||||
|
||||
/* ── intern table lifecycle ──────────────────────────────────────────── */
|
||||
|
||||
void sel_node_table_init(void)
|
||||
{
|
||||
kv_params_t params = {
|
||||
.nslots = 2003,
|
||||
.keyops = &sel_node_key_ops,
|
||||
.datakiller = NULL, /* GC sweep handles node freeing; kvtab must NOT
|
||||
* free values on fini */
|
||||
};
|
||||
|
||||
mylock_init(&sel_node_table.lock);
|
||||
sel_node_table.table = makeInstance(&kvtab_type, ¶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;
|
||||
}
|
||||
}
|
||||
260
src/lib/xce_select.peg
Normal file
260
src/lib/xce_select.peg
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
%prefix "xce_select"
|
||||
%value "xce_select_node_t *"
|
||||
%auxil "select_context_t *"
|
||||
%source {
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "xstring.h"
|
||||
|
||||
#define PCC_ERROR(auxil) xce_select_parse_error(auxil)
|
||||
|
||||
static void xce_select_parse_error(select_context_t *auxil) {
|
||||
auxil->has_error = 1;
|
||||
}
|
||||
|
||||
/* Resolve a resource name: clause-keyword check then resmgr lookup.
|
||||
* Returns a SEL_RESNAME node on success, NULL with has_error set on failure. */
|
||||
static xce_select_node_t* resolve_resname(select_context_t* auxil,
|
||||
const char* name)
|
||||
{
|
||||
static const char* clause_keywords[] = {
|
||||
"select", "rusage", "span", "same", "affinity", NULL
|
||||
};
|
||||
int i;
|
||||
for (i = 0; clause_keywords[i]; i++) {
|
||||
if (strcmp(name, clause_keywords[i]) == 0) {
|
||||
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
|
||||
"resource name '%s' conflicts with a clause keyword — "
|
||||
"use explicit select[%s ...] form if this is intentional",
|
||||
name, name);
|
||||
auxil->has_error = 1;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
xce_resource_t* r = NULL;
|
||||
int rc = key_find(auxil->resmgr, (void*)name, (void**)&r);
|
||||
if (rc <= 0 || r == NULL) {
|
||||
if (strcmp(name, "true") == 0 || strcmp(name, "false") == 0) {
|
||||
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
|
||||
"unknown resource '%s' — did you mean the boolean resource "
|
||||
"name directly (e.g. 'gpu') or a numeric literal (1/0)?",
|
||||
name);
|
||||
} else {
|
||||
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
|
||||
"unknown resource '%s'", name);
|
||||
}
|
||||
auxil->has_error = 1;
|
||||
return NULL;
|
||||
}
|
||||
res_type_t res_type = valtype_to_res_type(r->valtype);
|
||||
return (xce_select_node_t*)makeSelectNode(SEL_RESNAME,
|
||||
(uintptr_t)r->idx,
|
||||
(uintptr_t)res_type);
|
||||
}
|
||||
}
|
||||
%common {
|
||||
#include "kv.h"
|
||||
#include "resreq.h"
|
||||
|
||||
#define PCC_GETCHAR(auxil) \
|
||||
({ int c = (auxil->input[auxil->pos] != '\0') ? (int)auxil->input[auxil->pos ++]: -1; \
|
||||
if (auxil->pos > auxil->err_pos) auxil->err_pos = auxil->pos; \
|
||||
c; \
|
||||
})
|
||||
}
|
||||
|
||||
statement <- _ e:select_or _ EOF {
|
||||
auxil->root = e;
|
||||
$$ = e;
|
||||
}
|
||||
|
||||
select_or <- l:select_and _ "||" _ r:select_or {
|
||||
if (auxil->has_error || l == NULL || r == NULL) {
|
||||
$$ = NULL;
|
||||
} else {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(SEL_OR, (uintptr_t)l, (uintptr_t)r);
|
||||
if ($$ == NULL) {
|
||||
auxil->has_error = 1;
|
||||
if (sel_get_optimize_level() == 0) {
|
||||
sel_free_tree(l);
|
||||
sel_free_tree(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/ e:select_and { $$ = e; }
|
||||
|
||||
select_and <- l:select_not _ "&&" _ r:select_and {
|
||||
if (auxil->has_error || l == NULL || r == NULL) {
|
||||
$$ = NULL;
|
||||
} else {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(SEL_AND, (uintptr_t)l, (uintptr_t)r);
|
||||
if ($$ == NULL) {
|
||||
auxil->has_error = 1;
|
||||
if (sel_get_optimize_level() == 0) {
|
||||
sel_free_tree(l);
|
||||
sel_free_tree(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/ e:select_not { $$ = e; }
|
||||
|
||||
select_not <- "!" _ e:select_primary {
|
||||
if (auxil->has_error || e == NULL) {
|
||||
$$ = NULL;
|
||||
} else {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(SEL_NOT, (uintptr_t)e, 0);
|
||||
if ($$ == NULL) {
|
||||
auxil->has_error = 1;
|
||||
if (sel_get_optimize_level() == 0)
|
||||
sel_free_tree(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
/ e:select_primary { $$ = e; }
|
||||
|
||||
select_primary <- "(" _ e:select_or _ ")" { $$ = e; }
|
||||
/ e:comparison { $$ = e; }
|
||||
/ e:resname_leaf { $$ = e; }
|
||||
|
||||
comparison <- l:resname_node _ op:cmp_op _ r:value_node {
|
||||
/* SEL_DISCARD: free a speculatively allocated node that the grammar
|
||||
* action decides not to use. Only frees in level-0 (non-CSE) mode;
|
||||
* in CSE mode the intern table owns the node and GC reclaims it. */
|
||||
#define SEL_DISCARD(n) do { \
|
||||
if ((n) != NULL && sel_get_optimize_level() == 0) { \
|
||||
if ((n)->type == SEL_STR && (n)->u.strval != NULL) \
|
||||
dereference((n)->u.strval); \
|
||||
free(n); \
|
||||
(n) = NULL; \
|
||||
} \
|
||||
} while (0)
|
||||
if (auxil->has_error || l == NULL || r == NULL) {
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
$$ = NULL;
|
||||
} else {
|
||||
sel_node_type_t op_type = (sel_node_type_t)(uintptr_t)op;
|
||||
if (op_type == SEL_EQ || op_type == SEL_NE) {
|
||||
/* 4b-2: EQ/NE type validation */
|
||||
if (l->u.resname.res_type == RES_TYPE_BOOL &&
|
||||
r->type == SEL_STR) {
|
||||
/* BOOL resource vs string literal: reject */
|
||||
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
|
||||
"type mismatch: boolean resource cannot be compared "
|
||||
"with a string literal");
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
$$ = NULL;
|
||||
} else if (l->u.resname.res_type == RES_TYPE_STRING &&
|
||||
r->type == SEL_NUM) {
|
||||
/* Coerce SEL_NUM → SEL_STR when LHS is STRING type */
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.17g", r->u.numval);
|
||||
char* s = makeString(buf);
|
||||
if (s == NULL) {
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
$$ = NULL;
|
||||
} else {
|
||||
if (sel_get_optimize_level() == 0) free(r);
|
||||
r = (xce_select_node_t*)makeSelectNode(SEL_STR,
|
||||
(uintptr_t)s,
|
||||
0);
|
||||
if (r == NULL) {
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
$$ = NULL;
|
||||
} else {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(op_type,
|
||||
(uintptr_t)l,
|
||||
(uintptr_t)r);
|
||||
if ($$ == NULL) {
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(op_type,
|
||||
(uintptr_t)l,
|
||||
(uintptr_t)r);
|
||||
if ($$ == NULL) {
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 4b-3: Ordered comparison (GT/LT/GE/LE) type validation */
|
||||
if (l->u.resname.res_type != RES_TYPE_NUMERIC) {
|
||||
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
|
||||
"type mismatch: ordered comparison requires a numeric "
|
||||
"resource on the left-hand side");
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
$$ = NULL;
|
||||
} else if (r->type != SEL_NUM) {
|
||||
snprintf(auxil->errmsg, sizeof(auxil->errmsg),
|
||||
"type mismatch: ordered comparison requires a numeric "
|
||||
"literal on the right-hand side");
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
$$ = NULL;
|
||||
} else {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(op_type,
|
||||
(uintptr_t)l,
|
||||
(uintptr_t)r);
|
||||
if ($$ == NULL) {
|
||||
auxil->has_error = 1;
|
||||
SEL_DISCARD(l);
|
||||
SEL_DISCARD(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef SEL_DISCARD
|
||||
}
|
||||
|
||||
resname_node <- !("any" ![a-zA-Z_0-9]) < [a-zA-Z_][a-zA-Z_0-9]* > {
|
||||
$$ = auxil->has_error ? NULL : resolve_resname(auxil, $0);
|
||||
}
|
||||
|
||||
resname_leaf <- !("any" ![a-zA-Z_0-9]) < [a-zA-Z_][a-zA-Z_0-9]* > {
|
||||
$$ = auxil->has_error ? NULL : resolve_resname(auxil, $0);
|
||||
}
|
||||
|
||||
value_node <- "any" ![a-zA-Z_0-9] {
|
||||
$$ = (xce_select_node_t*)makeSelectNode(SEL_ANY, 0, 0);
|
||||
}
|
||||
/ < [0-9]+ ("." [0-9]*)? > {
|
||||
double v = strtod($0, NULL);
|
||||
uintptr_t bits;
|
||||
memcpy(&bits, &v, sizeof(bits));
|
||||
$$ = (xce_select_node_t*)makeSelectNode(SEL_NUM, bits, 0);
|
||||
}
|
||||
/ < [a-zA-Z_][a-zA-Z_0-9]* > {
|
||||
char* s = makeString((char*)$0);
|
||||
if (s == NULL) { auxil->has_error = 1; $$ = NULL; }
|
||||
else { $$ = (xce_select_node_t*)makeSelectNode(SEL_STR, (uintptr_t)s, 0); }
|
||||
}
|
||||
|
||||
cmp_op <- ">=" { $$ = (xce_select_node_t*)(uintptr_t)SEL_GE; }
|
||||
/ "<=" { $$ = (xce_select_node_t*)(uintptr_t)SEL_LE; }
|
||||
/ "!=" { $$ = (xce_select_node_t*)(uintptr_t)SEL_NE; }
|
||||
/ "==" { $$ = (xce_select_node_t*)(uintptr_t)SEL_EQ; }
|
||||
/ ">" { $$ = (xce_select_node_t*)(uintptr_t)SEL_GT; }
|
||||
/ "<" { $$ = (xce_select_node_t*)(uintptr_t)SEL_LT; }
|
||||
|
||||
_ <- [ \t]*
|
||||
|
||||
EOF <- !.
|
||||
Loading…
Add table
Add a link
Reference in a new issue