···11+# P4 — Parser & dispatch: design
22+33+**Status:** design locked 2026-06-02. Scope = port **all of `common/parse.c`** to Rust
44+(`ircd-common/src/parse.rs`) and **drop `parse.o` outright** (no `parse_link.o`): every
55+symbol parse.c defines is ported, so nothing C-side remains in that translation unit.
66+77+## Why a clean drop (not a `_link.o` partial port)
88+99+P1/P2/P3 used `<mod>_link.o` because each of those modules kept *some* function in C
1010+(`MyMalloc`, `m_whowas`, `m_hash`, the variadic senders). parse.c is different: **every**
1111+function it defines — the 11 public `find_*` wrappers, `parse`, `getfield`, the 4 exported
1212+stub handlers (`m_nop`/`m_nopriv`/`m_unreg`/`m_reg`), the global `msgtab[]`, and the 3
1313+file-statics (`find_sender`/`cancel_clients`/`remove_unknown`) — is portable to Rust now.
1414+The variadic *senders* it **calls** (`sendto_one`/`sendto_flag`/`sendto_serv_butone`) live in
1515+send.c/s_send.c and stay C; Rust **calls** them through bindgen variadic declarations exactly
1616+as `s_numeric.rs` already does (`c"..."` literal patterns). Calling a C variadic from Rust is
1717+stable; only *defining* one is not. So `parse.o` joins `PORTED` like `list.o`/`match.o`.
1818+1919+## What P4 ports (Rust) vs keeps (C)
2020+2121+| parse.c symbol | Disposition |
2222+| --- | --- |
2323+| `msgtab[]` | Rust `#[no_mangle] pub static mut msgtab: [Message; N]`, config-resolved table (see gates). Counters (`count`/`bytes`/…) mutate in place exactly as C. |
2424+| `find_client/find_uid/find_sid/find_service/find_server/find_mask/find_name/find_person/find_matching_client/find_userhost/find_target` | Rust. Thin wrappers over Rust `hash_find_*` (P2) + `match`/`mycmp`/`collapse` (P1). |
2525+| `parse` | Rust. The tokenizer + dispatcher. Calls C variadic senders + C `get_client_name`/`exit_client`/`is_allowed`; Rust `do_numeric`/`mycmp`/`replies[]`. |
2626+| `getfield` | Rust. Pure config-field splitter (static `line`). |
2727+| `m_nop/m_nopriv/m_unreg/m_reg` | Rust. Trivial stubs (3 call `sendto_one(replies[...])`). |
2828+| `find_sender/cancel_clients/remove_unknown` | Rust **private** fns (were file-static). Call C senders + `exit_client`. |
2929+| `para[MPAR+1]`, `sender[HOSTLEN+1]` | Rust module statics. `para` is shared between `find_sender` and `parse` (faithful: `find_sender` writes `para[0]`). |
3030+3131+`m_*` handlers referenced by `msgtab` (≈60: `m_private`, `m_join`, …) **stay C** (P5) and are
3232+`extern`-declared; the Rust `msgtab` initializer takes their function pointers
3333+(`Some(m_join as CmdHandler-fn)`), which is legal in a `static` initializer.
3434+3535+## Verified config gates (cbuild/config.h) — affect the compiled `msgtab`
3636+3737+| Gate | State | Effect on `msgtab` / `parse` |
3838+| --- | --- | --- |
3939+| `USE_SERVICES` | **off** | no `SERVSET` row |
4040+| `TKLINE` | **on** | `TKLINE` + `UNTKLINE` rows **present** |
4141+| `KLINE` | **off** | no `KLINE` row |
4242+| `ENABLE_SIDTRACE` | **off** | no `SIDTRACE` row |
4343+| `MOTD_UNREG` | **off** | `MOTD` unregistered-slot handler = `m_unreg` |
4444+| `IDLE_FROM_MSG` | **on** | `parse` idle-update guard uses `fhandler == m_private` (not the `!= m_ping && != m_pong` branch) |
4545+| `DEBUGMODE` | **off** | every `Debug((...))` is a no-op → omitted from the Rust port |
4646+4747+Numeric constants (struct_def.h): `MPAR`=15 (parse.c local), `HOSTLEN`=63, `SIDLEN`=4,
4848+`BOOT_PROT`=0x100, `FLUSH_BUFFER`=-2, `ACL_NOPENALTY`=0x20000. `STAT_MAX`=5 (handlers per row).
4949+5050+## Macro crib (inline-translate at use sites; no macros.rs — matches P1/P2/P3)
5151+5252+`status` is `i16`, compare as `c_int`. From struct_def.h:
5353+- `IsDead(x)` = `flags & FLAGS_DEADSOCK`
5454+- `IsServer(x)` = `status == STAT_SERVER`; `IsMe(x)` = `STAT_ME`; `IsClient(x)` = `STAT_CLIENT || STAT_OPER`
5555+- `IsPerson(x)` = `!user.is_null() && IsClient(x)`; `IsService(x)` = `status == STAT_SERVICE && !service.is_null()`
5656+- `IsRegistered(x)` = `status >= STAT_SERVER || status == STAT_ME`
5757+- `IsRegisteredUser(x)` = `(status == STAT_CLIENT || status == STAT_OPER) && !user.is_null()`
5858+- `MyConnect(x)` = `fd >= 0`
5959+- `BadPtr(x)` = `x.is_null() || *x == 0`; `BadTo(x)` = `if BadPtr(x) {c"*"} else {x}`
6060+- `ME` = `me.name`
6161+6262+## Rust→C variadic call surface (stable; pattern proven in s_numeric.rs)
6363+6464+`parse`/`cancel_clients`/`remove_unknown`/`m_*` call: `sendto_one(to, pat, ...)`,
6565+`sendto_flag(sch, pat, ...)`, `sendto_serv_butone(one, pat, ...)` — all bindgen-declared
6666+variadics. Plus libc `sprintf`/`strcpy`/`strlen`/`strchr`(for `index`)/`isdigit`. `replies[i]`
6767+is the Rust `s_err` static (`[*const c_char; 1000]`); pass `replies[ERR_*]` as the pattern.
6868+6969+## Build wiring
7070+7171+`ircd-sys/build.rs`: add `"parse.o"` to `PORTED`. `parse.o` stays in `CREF_OBJS` (the oracle
7272+still compiles the pristine `parse.c` → `cref_parse`/`cref_msgtab`/`cref_find_client`/…). No new
7373+second-compile. `ircd-common/src/lib.rs`: add `pub mod parse;` + `parse::PARSE_LINK_ANCHOR`.
7474+7575+## L1 test strategy (zero-diff vs `cref_*`)
7676+7777+`ircd-testkit/tests/parse_diff.rs`. The cref oracle exports `cref_msgtab`, `cref_find_client`,
7878+`cref_find_mask`, `cref_find_name`, `cref_getfield`, `cref_parse`, etc.
7979+8080+- **msgtab structure**: extern both `msgtab` and `cref_msgtab` as `[Message; N]`; walk until the
8181+ NULL-`cmd` terminator on BOTH; assert equal length and, per row, `strcmp(cmd)==0` +
8282+ identical `minparams`/`maxparams`. Handler **pointers are NOT compared** (Rust row points at
8383+ `m_join`, oracle row at `cref_m_join`); structural identity + the parse fuzz/dispatch test
8484+ below cover dispatch. Assert all counters start 0.
8585+- **getfield**: drive identical `ircd.conf`-style lines (IRCDCONF_DELIMITER is `'|'`,
8686+ config.h:500 — NOT `':'`, an L1-caught bug; escaped `\|`, trailing field, empty
8787+ field) through `getfield`/`cref_getfield`; assert identical returned fields and
8888+ identical re-entrant continuation (`getfield(NULL)` sequence).
8989+- **find_mask / find_name**: build with no hash state (returns the cptr/fallback paths) — assert
9090+ identical pointer-class results on `*`-containing and plain names. (Hash-populated lookups are
9191+ L2: they need a live hash table shared between the two symbol sets.)
9292+- **tokenizer (the core dispatch path)**: this is the L3-fuzz gate per PLAN. For L1, call
9393+ `parse(cptr, buf, bufend)` vs `cref_parse` on a corpus of raw lines exercising: leading/trailing
9494+ colon prefix, empty message, numeric (3-digit) command, unknown command, `MPAR` parameter cap,
9595+ empty trailing param, 512-byte truncation framing. Because `parse` mutates `cptr`/`ircstp` and
9696+ dispatches into still-C `m_*`, **full** differential here needs a populated server (L2). L1
9797+ asserts the **pure tokenizer-visible** outcomes that don't require a live `me`/hash: the
9898+ `para[]` split + return code on the no-prefix / non-dispatching paths (numeric with null target,
9999+ empty, unknown-command-from-non-person). The full post-state (`aClient` bytes + `ircstp`
100100+ counters + handler identity) diff is the **L3 fuzz** gate, run when the harness has a populated
101101+ server fixture.
102102+103103+The complete `parse()` post-state differential (mutated `aClient`, `ircstp`, dispatch identity)
104104+and the hash-populated `find_*` lookups are the **L3/L2** gates (post-P4), matching how P2/P3
105105+deferred sender-wire and live-state checks.
106106+107107+## Deliverable: P5 handler-cluster analysis
108108+109109+PLAN lists "per-handler-file static-symbol cluster analysis" as a P4 deliverable (P5 prereq).
110110+Produced as `docs/superpowers/specs/2026-06-02-p5-handler-cluster-map.md`.