Skip to content

Extract shared RESP.encode-command for the command wire format - #15

Merged
hellerve merged 1 commit into
masterfrom
claude/resp-encode-command
Jul 23, 2026
Merged

Extract shared RESP.encode-command for the command wire format#15
hellerve merged 1 commit into
masterfrom
claude/resp-encode-command

Conversation

@carpentry-agent

Copy link
Copy Markdown

What

Redis.send (direct send) and RedisPipeline.add (batched send) each built the byte-identical RESP multi-bulk wire string inline:

(str &(RESP.Arr (concat &[(copy-map ... (Pattern.split #" " cmd)) (copy-map ... args)])))

This extracts that into one public function, RESP.encode-command, and routes both call sites through it.

  • Home: the RESP module — the wire-format module, where to-redis (the definterface) and RESP.str already live, so it can see everything it needs. Public, so both Redis and RedisPipeline reach it without tripping the cross-module private visibility difference between local Carp and CI.
  • Signature: (Fn [&String &(Array RESP)] String). Both call sites already pass cmd as an owned String and args as &(Array RESP) — the defredis macro and pipeline-cmd- both build (ref (array (to-redis …))), i.e. an array of already-encoded RESP values — so the element type is RESP, not (Box RESP). (No sig form is added, since the file uses none; the type is fully pinned by the two concrete call sites.)
  • Redis.send → encode via the helper, then TcpStream.send.
  • RedisPipeline.add → encode via the helper, then StringBuf.append-str, bump count.

Why

The encoding was a duplicated copy in two places. Any future change to how commands are serialized (inline vs multi-bulk, escaping, …) had to be made in both or the two paths would silently diverge. One RESP.encode-command gives a single source of truth for the command wire format and makes the pipeline path provably identical to the direct-send path.

Behavior-preserving

No wire-format change — the bytes produced are identical.

  • The existing pipeline byte assertions (SET k v, GET foo, multi-command concatenation, no-arg PING) go through RedisPipeline.add, which now delegates to RESP.encode-command, and all still pass unchanged.

  • Added three pure encoder tests (no live server) pinning the wire bytes directly:

    • SET foo bar*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
    • CLIENT ID (multi-word name, no args) → *2\r\n$6\r\nCLIENT\r\n$2\r\nID\r\n
    • CONFIG GET maxmemory (multi-word name + arg) → *3\r\n$6\r\nCONFIG\r\n$3\r\nGET\r\n$9\r\nmaxmemory\r\n

    The multi-word command path (Pattern.split #" ") was previously untested.

Verification

  • carp -x test/resp.carp134 passed, 0 failed
  • carp -b examples/simple.carp builds
  • carp-fmt --check and angler clean on redis.carp and test/resp.carp
  • carp -x gendocs.carp runs without error

Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

Redis.send and RedisPipeline.add each built the identical RESP multi-bulk
wire string inline. Any future change to how commands are serialized had to
be made in both call sites or they would silently diverge.

Consolidate the encoding into a public RESP.encode-command in the wire-format
module (where to-redis and RESP.str already live) and route both call sites
through it, giving one source of truth for the command wire format. No
behavior change: the bytes produced are identical.

Add pure encoder tests pinning the wire bytes for a command with arguments,
a space-separated command name (CLIENT ID), and a multi-word command with an
argument (CONFIG GET). The existing pipeline byte assertions still pass,
confirming the refactor is behavior-preserving.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Checked out claude/resp-encode-command (470566f, based on current master) on armhf:

  • carp -x test/resp.carp134/0 (all three new encode-command tests pass).
  • carp -b examples/simple.carp → exit 0, no errors (only the pre-existing benign No 'prn' function for StringBuf warning, unrelated to this change).
  • CI: test (ubuntu-latest) + test (macos-latest) both green.
  • No CHANGELOG file exists and this is an internal refactor — correctly declined an entry.

Findings

No findings — this is a clean, genuinely behavior-preserving extraction.

  • Byte-identity, read through. The new RESP.encode-command performs the same operations the two inline sites did (to-redis-map + Box.init, Array.concat, str &(Arr …)); it's a straight relocation. The type note is right — args is &(Array RESP) (elements already encoded by the defredis/pipeline-cmd- layer), not &(Array (Box RESP)).
  • Non-vacuity, independently verified. I mutated the helper's Pattern.split #" "#"_": SET foo bar still passed but both multi-word tests (CLIENT ID, CONFIG GET maxmemory) failed (132/2). So the space-split path — previously untested — is now genuinely covered, and the harness catches divergence. Reverted.
  • Refactor is behavior-locked both ways. RedisPipeline.add now routes through the helper, and the pre-existing pipeline byte assertions still pass unchanged → add's wire output is provably identical. Redis.send shares that same helper (its only change is the one-line call), so it's transitively byte-identical to the old inline encoder.
  • Visibility is correct. Kept encode-command public in the RESP wire-format module (alongside to-redis/RESP.str), so both Redis and RedisPipeline reach it without the local-looser-than-CI private cross-module trap — both CI OSes green confirm it.

Verdict: merge

Minimal (+31/-11, 2 files), correct, and behavior-preserving with the right kind of proof — the shared helper makes the pipeline and direct-send paths a single source of truth. Draft is intentional; leaving it for you to un-draft.

@hellerve
hellerve marked this pull request as ready for review July 23, 2026 16:05
@hellerve
hellerve merged commit f375b06 into master Jul 23, 2026
2 checks passed
@hellerve
hellerve deleted the claude/resp-encode-command branch July 23, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant