docs: address document-review findings for resreq-ast-cse-design

Multi-persona review (coherence, feasibility, adversarial, product-lens)
identified 25 findings. Applied 14 fixes:

- Fix makeSelectNode key_add return code bug (rc2 → rc3)
- Resolve derefSelectNode contradiction: remove function and refcount
  field, adopt GC-only reclamation model
- Add boolean_literal grammar rule (true/false keywords) and
  SEL_BOOL_LITERAL node type + bval union member
- Clarify transform pipeline: makeSelectNode does commutativity swap
  internally; flattening/const-fold are post-parse tree walks
- Complete const-fold rules with annihilation laws
- Add SEL_NUM bit-cast semantics (±0 distinction, NaN payloads)
- Add resmgr=NULL fallback behavior spec
- Add runtime type safety assertions for debug builds
- Add GC trigger and session-based scheduler context
- Fix section numbering gap (renumber §5.6 → §5.5)
- Add gc_sweep_select_nodes/gc_mark_tree to Phase 2 declarations
- Update kill_primary_resreq instruction for GC model
- Add any keyword semantic inconsistency note (!= vs !(==))

4 open issues remain for implementation phase: string resource
interface, single-goroutine enforcement, kvtab iteration API,
CSE benefit quantification.
This commit is contained in:
Rongsong Shen 2026-04-21 03:29:31 +00:00
parent 33b76d9a77
commit 877741d008

View file

@ -101,10 +101,11 @@ atom_expr ::= '(' or_expr ')' | comparison | resname
comparison ::= resname cmp_op value comparison ::= resname cmp_op value
cmp_op ::= '>=' | '<=' | '!=' | '==' | '>' | '<' cmp_op ::= '>=' | '<=' | '!=' | '==' | '>' | '<'
resname ::= [a-zA-Z_][a-zA-Z_0-9]* resname ::= [a-zA-Z_][a-zA-Z_0-9]*
value ::= string_literal | number | ANY value ::= string_literal | number | boolean_literal | ANY
boolean_literal ::= 'true' | 'false'
ANY ::= 'any' ANY ::= 'any'
number ::= [0-9]+ ('.' [0-9]*)? number ::= [0-9]+ ('.' [0-9]*)?
string_literal ::= [a-zA-Z_][a-zA-Z_0-9]* (unquoted word, excluding 'any') string_literal ::= [a-zA-Z_][a-zA-Z_0-9]* (unquoted word, excluding 'any', 'true', 'false')
``` ```
**LSF-compatible disambiguation:** `resname` and `string_literal` share **LSF-compatible disambiguation:** `resname` and `string_literal` share
@ -121,6 +122,23 @@ only valid as the right-hand side of `==` or `!=` (e.g., `type == any`,
The const-fold pass (§6.3) reduces `resname == any` to `SEL_TRUE`, so The const-fold pass (§6.3) reduces `resname == any` to `SEL_TRUE`, so
`eval_select` never encounters `SEL_ANY` at runtime. `eval_select` never encounters `SEL_ANY` at runtime.
**Note on `!= any`:** `resname != any` is a parse error. However,
`!(resname == any)` const-folds to `!SEL_TRUE` which reduces to
`SEL_FALSE` — these are semantically equivalent. This inconsistency
exists because the parse-time rejection of `!= any` provides a clearer
error message for users, while the const-fold path handles the negated
form implicitly. If this inconsistency proves confusing in practice,
a future change can allow `resname != any` at parse time and const-fold
it to `SEL_FALSE`.
**Boolean literals:** `true` and `false` are parsed as `SEL_BOOL_LITERAL`
nodes. The value is stored as `int bval` (1 for true, 0 for false) in
the node's union. Type validation ensures that boolean literals only
appear in `==`/`!=` comparisons with `RES_TYPE_BOOL` resources, or as
standalone boolean operands (where they const-fold to `SEL_TRUE` or
`SEL_FALSE`). `true` and `false` are reserved keywords excluded from
the `string_literal` pattern.
### 3.2 Node Types ### 3.2 Node Types
```c ```c
@ -137,6 +155,7 @@ typedef enum {
SEL_RESNAME, /* leaf: resource variable */ SEL_RESNAME, /* leaf: resource variable */
SEL_NUM, /* leaf: numeric literal */ SEL_NUM, /* leaf: numeric literal */
SEL_STR, /* leaf: string literal */ SEL_STR, /* leaf: string literal */
SEL_BOOL_LITERAL, /* leaf: boolean literal (true/false) */
SEL_ANY, /* leaf: any keyword (const-folded to SEL_TRUE) */ SEL_ANY, /* leaf: any keyword (const-folded to SEL_TRUE) */
SEL_TRUE, /* leaf: constant true (from const fold) */ SEL_TRUE, /* leaf: constant true (from const fold) */
SEL_FALSE, /* leaf: constant false (from const fold) */ SEL_FALSE, /* leaf: constant false (from const fold) */
@ -148,7 +167,6 @@ typedef enum {
```c ```c
struct xce_select_node_ { struct xce_select_node_ {
sel_node_type_t type; sel_node_type_t type;
atomic_ulong refcount; /* intern table reference count */
uint32_t hashcode; /* cached at construction */ uint32_t hashcode; /* cached at construction */
unsigned gc_mark; /* set during mark-and-sweep GC (§5.6) */ unsigned gc_mark; /* set during mark-and-sweep GC (§5.6) */
union { union {
@ -162,6 +180,7 @@ struct xce_select_node_ {
} resname; } resname;
double numval; /* SEL_NUM */ double numval; /* SEL_NUM */
char* strval; /* SEL_STR — interned via makeString() */ char* strval; /* SEL_STR — interned via makeString() */
int bval; /* SEL_BOOL_LITERAL — 1 for true, 0 for false */
} u; } u;
}; };
``` ```
@ -279,10 +298,17 @@ Key encoding per node type:
| `SEL_RESNAME` | `(uintptr_t)res_idx` | `0` | | `SEL_RESNAME` | `(uintptr_t)res_idx` | `0` |
| `SEL_NUM` | bit-cast of `double` | `0` | | `SEL_NUM` | bit-cast of `double` | `0` |
| `SEL_STR` | `(uintptr_t)xstring_ptr` | `0` | | `SEL_STR` | `(uintptr_t)xstring_ptr` | `0` |
| `SEL_BOOL_LITERAL` | `(uintptr_t)bval` | `0` |
| `SEL_ANY` | `0` | `0` | | `SEL_ANY` | `0` | `0` |
| `SEL_TRUE` | `0` | `0` | | `SEL_TRUE` | `0` | `0` |
| `SEL_FALSE` | `0` | `0` | | `SEL_FALSE` | `0` | `0` |
`SEL_NUM` uses the exact bit-pattern of the `double` value reinterpreted as
`uintptr_t` (via `memcpy` or union cast). Two numeric literals are
equivalent for CSE if their bit-patterns are identical; `0.0` and `-0.0`
are distinct CSE keys (they differ in the sign bit). NaN payloads are
also compared by bit-pattern.
`SEL_STR` uses the interned `xstring` pointer — since `makeString()` already `SEL_STR` uses the interned `xstring` pointer — since `makeString()` already
canonicalizes strings (libxce's string de-dup mechanism maps identical canonicalizes strings (libxce's string de-dup mechanism maps identical
strings to the same pointer), pointer equality IS string equality. strings to the same pointer), pointer equality IS string equality.
@ -322,9 +348,8 @@ xce_select_node_t* makeSelectNode(sel_node_type_t type,
xce_select_node_t* node = NULL; xce_select_node_t* node = NULL;
mylock_lock(&sel_node_table.lock); mylock_lock(&sel_node_table.lock);
int rc = key_find(sel_node_table.table, &key, (void**)&node); int rc = key_find(sel_node_table.table, &key, (void**)&node);
if (rc > 0) reference(node); if (rc > 0) { mylock_unlock(&sel_node_table.lock); return node; }
mylock_unlock(&sel_node_table.lock); mylock_unlock(&sel_node_table.lock);
if (rc > 0) return node; /* cache hit */
if (rc < 0) return NULL; /* key_find error (not ENOENT) */ if (rc < 0) return NULL; /* key_find error (not ENOENT) */
/* 4. alloc and fill */ /* 4. alloc and fill */
@ -332,7 +357,6 @@ xce_select_node_t* makeSelectNode(sel_node_type_t type,
if (node == NULL) { errno = ENOMEM; return NULL; } if (node == NULL) { errno = ENOMEM; return NULL; }
node->type = type; node->type = type;
node->hashcode = sel_key_hash(&key); node->hashcode = sel_key_hash(&key);
atomic_store(&node->refcount, 1);
/* fill u.* from op1/op2 by type … */ /* fill u.* from op1/op2 by type … */
/* 5. insert — re-check under lock for TOCTOU safety */ /* 5. insert — re-check under lock for TOCTOU safety */
@ -341,14 +365,13 @@ xce_select_node_t* makeSelectNode(sel_node_type_t type,
int rc2 = key_find(sel_node_table.table, &key, (void**)&existing); int rc2 = key_find(sel_node_table.table, &key, (void**)&existing);
if (rc2 > 0) { if (rc2 > 0) {
/* another thread inserted first — use theirs, free ours */ /* another thread inserted first — use theirs, free ours */
reference(existing);
mylock_unlock(&sel_node_table.lock); mylock_unlock(&sel_node_table.lock);
free(node); free(node);
return existing; return existing;
} }
key_add(sel_node_table.table, &key, node); int rc3 = key_add(sel_node_table.table, &key, node);
if (rc2 < 0) { if (rc3 < 0) {
/* key_add failed (EEXIST check already passed, likely ENOMEM) */ /* key_add failed (likely ENOMEM or EEXIST from a race) */
mylock_unlock(&sel_node_table.lock); mylock_unlock(&sel_node_table.lock);
free(node); free(node);
return NULL; return NULL;
@ -365,28 +388,18 @@ select intern table adds the re-check to prevent node leaks under
concurrent construction. An alternative is to hold the lock through concurrent construction. An alternative is to hold the lock through
the entire sequence (simpler but longer critical section). the entire sequence (simpler but longer critical section).
```c `kill_primary_resreq()` in `resreq.c` must unlink the select root
void derefSelectNode(xce_select_node_t* node) pointer (`primary->select->root = NULL`) before freeing the select struct.
{ Node reclamation is handled by mark-and-sweep GC (§5.5) — no explicit
if (node == NULL) return; deref function is needed.
atomic_fetch_sub(&node->refcount, 1);
/* Reclamation is handled by mark-and-sweep GC (§5.6).
When a job completes, kill_primary_resreq calls derefSelectNode
on the root to decrement refcounts. The GC sweep between
sessions frees nodes that are no longer reachable from any
live job's resreq root. */
}
```
`kill_primary_resreq()` in `resreq.c` must call ### 5.5 Mark-and-Sweep GC Between Scheduling Sessions
`derefSelectNode(primary->select->root)` before freeing the select struct.
### 5.6 Mark-and-Sweep GC Between Scheduling Sessions Node reclamation is handled by a **mark-and-sweep** GC that runs
between scheduling sessions, when no evaluation is in progress. There
`derefSelectNode` only decrements refcounts — it does **not** free nodes is no `derefSelectNode` function — when a job completes, its resreq
or remove them from the intern table. Reclamation is handled by a entry is simply unlinked from the job (see §5.4); the next GC sweep
**mark-and-sweep** GC that runs between scheduling sessions, when no reclaims unreachable nodes.
evaluation is in progress:
``` ```
gc_sweep_select_nodes() gc_sweep_select_nodes()
@ -409,14 +422,22 @@ This approach is simple and correct:
- A node is reachable if and only if it is visited from at least one live - A node is reachable if and only if it is visited from at least one live
job's resreq root. No reference provenance tracking is needed — the job's resreq root. No reference provenance tracking is needed — the
mark flag is a single boolean per node. mark flag is a single boolean per node.
- `derefSelectNode` is removed — when a job completes, its resreq entry
is simply unlinked from the job; the next GC sweep reclaims unreachable
nodes.
- The sweep runs only between sessions (no concurrent eval), so no - The sweep runs only between sessions (no concurrent eval), so no
read-after-free risk exists. read-after-free risk exists.
- `SEL_STR` nodes' `xstring` references are handled by the same mark - `SEL_STR` nodes' `xstring` references are handled by the same mark
pass — if the SEL_STR node is reachable, the string stays interned. pass — if the SEL_STR node is reachable, the string stays interned.
The scheduler is responsible for calling `gc_sweep_select_nodes()`
between scheduling sessions, typically after all dispatch/defer decisions
for the current batch of jobs have been made. The function is not
thread-safe with respect to concurrent `eval_select` calls, so the
scheduler must ensure no evaluations are in progress when GC runs.
Like IBM LSF, the XCE scheduler operates in sessions — each session
evaluates all pending jobs against candidate hosts, dispatches what it
can, then ends. Pre/post-session work (such as GC) is a natural fit
for this architecture.
**Pointer-based canonicalization note:** Sorting by `uintptr_t` produces a **Pointer-based canonicalization note:** Sorting by `uintptr_t` produces a
deterministic total order within a single process lifetime. Across deterministic total order within a single process lifetime. Across
process restarts, pointer addresses change (ASLR, heap layout), so the process restarts, pointer addresses change (ASLR, heap layout), so the
@ -472,9 +493,9 @@ are statically determined at parse time:
resource value). The `any` keyword (§3.1) is only valid on the resource value). The `any` keyword (§3.1) is only valid on the
right-hand side of `==` or `!=`. For `!=`, `resname != any` is a right-hand side of `==` or `!=`. For `!=`, `resname != any` is a
parse error (nothing would not match any). parse error (nothing would not match any).
- `SEL_TRUE && X` → `X`, `X && SEL_TRUE` → `X` (identity elimination) - Identity laws:`SEL_TRUE && X` → `X`, `X && SEL_TRUE` → `X`; `SEL_FALSE || X` → `X`, `X || SEL_FALSE` → `X`
- `SEL_FALSE || X` → `X`, `X || SEL_FALSE` → `X` (identity elimination) - Annihilation laws: `SEL_FALSE && X` → `SEL_FALSE`, `X && SEL_FALSE` → `SEL_FALSE`; `SEL_TRUE || X` → `SEL_TRUE`, `X || SEL_TRUE` → `SEL_TRUE`
- `!SEL_TRUE` → `SEL_FALSE`, `!SEL_FALSE` → `SEL_TRUE` - Complement laws: `!SEL_TRUE` → `SEL_FALSE`, `!SEL_FALSE` → `SEL_TRUE`
The const-fold pass runs **after** construction and flattening, replacing The const-fold pass runs **after** construction and flattening, replacing
subtrees with constant leaf nodes. Since all nodes are interned, subtrees with constant leaf nodes. Since all nodes are interned,
@ -491,17 +512,23 @@ AST construction follows a fixed pipeline after the PEG parser produces
the raw tree: the raw tree:
``` ```
1. Parse (PEG → raw binary AST) 1. Parse (PEG → raw binary AST, each PEG action calls `makeSelectNode`
2. Commutativity canonicalization (operand swap, §6) which performs commutativity canonicalization as part of CSE key
3. Associativity flattening (§6.2) construction)
4. Const-fold (§6.3) 2. Post-parse tree walk: associativity flattening (§6.2)
5. CSE intern (makeSelectNode for each node bottom-up) 3. Post-parse tree walk: const-fold (§6.3)
4. Bottom-up re-intern via `makeSelectNode` for any modified subtrees
``` ```
Steps 2–4 operate on the tree before interning (they construct new Note: commutativity canonicalization (operand swap) happens *inside*
nodes via `makeSelectNode` which itself does the commutativity swap). `makeSelectNode` as part of the CSE key lookup — it is not a separate
Step 5 re-interns any modified subtrees, ensuring CSE maximizes sharing pipeline step. The PEG action for each binary operator calls
on the fully normalized tree. `makeSelectNode`, which sorts `op1`/`op2` for commutative operators
before building the key. Flattening and const-fold are post-parse tree
walks that traverse the raw AST and call `makeSelectNode` to construct
new normalized subtrees. Since `makeSelectNode` already handles interning
and canonicalization, these walks naturally produce the maximally shared
tree.
--- ---
@ -578,6 +605,14 @@ See §12 for the formal declaration.
Unknown resource names fail the parse (rejected at job submission time). Unknown resource names fail the parse (rejected at job submission time).
**resmgr=NULL fallback:** When `resmgr` is NULL (as called by the
existing `parse_resreq_primary` entry point), the parser cannot
disambiguate resname from string_literal. In this mode, all bare
words are treated as `SEL_STR` and no type validation is performed —
the caller must validate type compatibility before evaluation. The
new entry point `parse_resreq_primary_with_resmgr` should be preferred
whenever the resource manager is available.
The **resmgr** (resource manager) is simply an array of resource The **resmgr** (resource manager) is simply an array of resource
definitions — each entry maps a resource name to its type and other definitions — each entry maps a resource name to its type and other
metadata. The API server has access to this information via the cluster metadata. The API server has access to this information via the cluster
@ -712,6 +747,11 @@ int eval_select(const xce_select_node_t* node, const xvec_idval_t* host_resvals)
both operands of EQ/NE are type-compatible: either both numeric, both both operands of EQ/NE are type-compatible: either both numeric, both
string, or a RESNAME paired with a literal whose type matches the string, or a RESNAME paired with a literal whose type matches the
resource's declared type. See §7 for type coercion rules. resource's declared type. See §7 for type coercion rules.
- **Runtime type safety:** In debug builds, `eval_select` should include
`assert()` checks on operand types for `SEL_GT`/`SEL_LT`/`SEL_GE`/`SEL_LE`
branches to verify both operands are `RES_TYPE_NUMERIC`. In production
builds, these branches return `-1` on type mismatch as a defensive fallback,
since parse-time validation should prevent such cases from reaching eval.
### 8.4 Thread Safety ### 8.4 Thread Safety
@ -803,8 +843,8 @@ Key properties:
- Parse: valid expressions, unknown resource names (expect error), type - Parse: valid expressions, unknown resource names (expect error), type
mismatches (numeric op on string resource, expect error). mismatches (numeric op on string resource, expect error).
- CSE: `A && B` and `B && A` produce the same `xce_select_node_t*`. - CSE: `A && B` and `B && A` produce the same `xce_select_node_t*`.
- CSE: two calls to `makeSelectNode` with same args return same pointer, - CSE: two calls to `makeSelectNode` with same args return same pointer
refcount increases. (same interned node).
- Eval: numeric `>`, `<`, `==`; string `==`; bool standalone; `!`, `&&`, - Eval: numeric `>`, `<`, `==`; string `==`; bool standalone; `!`, `&&`,
`||`; short-circuit evaluation. `||`; short-circuit evaluation.
@ -818,8 +858,9 @@ signatures and assertions are defined before implementation code.*
- Add `res_type_t`, `sel_node_type_t`, `xce_select_node_t` typedef + - Add `res_type_t`, `sel_node_type_t`, `xce_select_node_t` typedef +
struct. struct.
- Fill `struct xce_resreq_select_` with `xce_select_node_t* root`. - Fill `struct xce_resreq_select_` with `xce_select_node_t* root`.
- Declare `makeSelectNode`, `derefSelectNode`, `eval_select`, - Declare `makeSelectNode`, `eval_select`,
`sel_node_table_init`, `sel_node_table_fini`. `sel_node_table_init`, `sel_node_table_fini`, `gc_sweep_select_nodes`,
`gc_mark_tree`.
- Add `res_type_to_valtype` / `valtype_to_res_type` mapping helpers. - Add `res_type_to_valtype` / `valtype_to_res_type` mapping helpers.
- Add `parse_resreq_primary_with_resmgr(primary, resreqString, errmsg, resmgr)` - Add `parse_resreq_primary_with_resmgr(primary, resreqString, errmsg, resmgr)`
as a new entry point. The existing `parse_resreq_primary` calls it as a new entry point. The existing `parse_resreq_primary` calls it
@ -843,7 +884,6 @@ signatures and assertions are defined before implementation code.*
- `sel_node_table_init(int nslots)` / `sel_node_table_fini()`. - `sel_node_table_init(int nslots)` / `sel_node_table_fini()`.
- `makeSelectNode` (§5.4) — with key_add return code check for TOCTOU - `makeSelectNode` (§5.4) — with key_add return code check for TOCTOU
safety. safety.
- `derefSelectNode` (§5.5).
- `sel_key_of`, `sel_key_hash` helpers. - `sel_key_of`, `sel_key_hash` helpers.
- `eval_select` (§8.3) — explicit error propagation (no C boolean ops). - `eval_select` (§8.3) — explicit error propagation (no C boolean ops).
- `host_resval_typed`, `host_get_bool`, `host_get_float`, - `host_resval_typed`, `host_get_bool`, `host_get_float`,
@ -851,7 +891,8 @@ signatures and assertions are defined before implementation code.*
- Mark-and-sweep GC: `gc_sweep_select_nodes()`, `gc_mark_tree()`. - Mark-and-sweep GC: `gc_sweep_select_nodes()`, `gc_mark_tree()`.
- AST transform pipeline: flatten, const-fold (§6.2–6.4). - AST transform pipeline: flatten, const-fold (§6.2–6.4).
- `xce_str_match` wrapper (initially `strcmp` semantics). - `xce_str_match` wrapper (initially `strcmp` semantics).
- Update `kill_primary_resreq` to call `derefSelectNode`. - Update `kill_primary_resreq` to unlink select root (`primary->select->root = NULL`)
before freeing the select struct. GC between sessions reclaims unreachable nodes.
- Update `parse_resreq_primary` to call `parse_select` with resmgr. - Update `parse_resreq_primary` to call `parse_select` with resmgr.
#### Phase 5: Build wiring #### Phase 5: Build wiring
@ -887,16 +928,15 @@ typedef enum { RES_TYPE_NUMERIC = 0, RES_TYPE_STRING, RES_TYPE_BOOL } res_type_t
typedef enum { typedef enum {
SEL_AND, SEL_OR, SEL_NOT, SEL_AND, SEL_OR, SEL_NOT,
SEL_GT, SEL_LT, SEL_GE, SEL_LE, SEL_EQ, SEL_NE, SEL_GT, SEL_LT, SEL_GE, SEL_LE, SEL_EQ, SEL_NE,
SEL_RESNAME, SEL_NUM, SEL_STR, SEL_ANY, SEL_TRUE, SEL_FALSE, SEL_RESNAME, SEL_NUM, SEL_STR, SEL_BOOL_LITERAL, SEL_ANY, SEL_TRUE, SEL_FALSE,
} sel_node_type_t; } sel_node_type_t;
typedef struct xce_select_node_ xce_select_node_t; typedef struct xce_select_node_ xce_select_node_t;
struct xce_select_node_ { struct xce_select_node_ {
sel_node_type_t type; sel_node_type_t type;
atomic_ulong refcount;
uint32_t hashcode; uint32_t hashcode;
unsigned gc_mark; /* set during mark-and-sweep GC (§5.6) */ unsigned gc_mark; /* set during mark-and-sweep GC (§5.5) */
union { union {
struct { xce_select_node_t *left, *right; } binop; struct { xce_select_node_t *left, *right; } binop;
xce_select_node_t* operand; xce_select_node_t* operand;