Engineering Note

Before Asking an AI Agent for a Red-Black Tree: Define the Operating Conditions

What should be explicit when coding with an AI agent? A C shared-memory design illustrates constraints, invariants, test scope, and the reasons worth keeping alongside code.

Published · Jungwook Song ai-agentscdata-structuresmemoryarchitecture

Suppose you ask an AI agent to implement a red-black tree in C. The result supports lookup, insertion and deletion, and its tests pass. Will it also work appropriately inside shared memory used by several processes?

If that requirement was absent, allocating each new node with malloc() would be a reasonable implementation. Checking an algorithm’s behavior and checking its suitability for a particular environment are separate tasks.

An earlier C transaction-system project on HP-UX combined array-backed slots, a red-black tree and a free list. Revisiting that design highlights three things to make explicit when coding with an AI agent: environmental constraints, conditions that must remain true, and evidence that those conditions hold.

An empty arched recess in a textured wall with soft leaf shadows
Understanding the limits of a space helps determine what belongs inside it.

What a data-structure name leaves unspecified

Several processes needed to read and update the same transaction state. We kept it in shared memory. An ordinary malloc() call allocates from the calling process’s heap; it does not find an empty location inside an attached shared-memory segment.

Dynamic allocation within shared memory is possible with a suitable allocator. Our approach was to reserve a region, divide it into fixed-size slots, and link empty slots into a free list. An insertion took a slot; deletion returned storage for reuse.

The toolchain was constrained too. GCC was unavailable to the project: the code had to build with the HP-UX C compiler. Any reused implementation or library needed validation in that environment.

“Implement a red-black tree” communicates none of this. An agent may discover some constraints in the repository and build configuration, but that does not establish that it has found every relevant condition. Confirmed constraints and unresolved assumptions need to be distinguished before implementation proceeds.

Choose storage and lookup separately

Once storage was available, entries still needed to be found by key. The original implementation used a red-black tree for that job.

ComponentRoleStill to decide
Shared array slotsProvide fixed-size storageCapacity and address representation
Free listAcquire and return empty slotsExhaustion and synchronization
Red-black treeLook up, insert and erase keysNode storage and reference lifetime

The array was not sorted by key. Key 40 could occupy slot 0, while key 20 occupied slot 3. Tree links represented key relationships; slot indices represented storage locations.

Keys 40, 20 and 60 form a tree independently of their array positions, while empty slots form a separate free list
Lookup and storage management have separate roles within one region. Management slots are omitted.

A red-black tree maintains balance to support lookup, insertion and deletion in O(log n). Taking or returning the head of a free list is O(1). Adding a pool does not change tree-search complexity. String comparison, synchronization and data copying still contribute costs.

I do not remember the exact comparison that led to choosing a red-black tree. The code shows primarily keyed state lookup and updates, with shared slots reused for storage. Keeping search costs from becoming badly skewed as entries change is a plausible rationale, but there is no evidence here that it outperformed a hash table.

That distinction matters when reviewing alternatives with an agent. Keep the constraints explicit while allowing the solution to change. Shared-memory allocation does not require red-black-tree indexing. Exact-key lookup may suit a hash table and pool; dense integer keys may suit direct indexing; infrequent updates may make a sorted array worth comparing.

Add invariants to the implementation request

“Build a free list” leaves important behavior unspecified. More useful requirements include:

  • A slot linked into the tree must not also belong to the free list.
  • A failed insertion must not leave a reserved slot behind.
  • Deletion must return the slot physically unlinked by the tree implementation.
  • A slot must not be reused while another operation still uses it.
  • Exhaustion must follow an agreed failure or waiting policy.

A condition that must remain true, such as tree membership and free-list membership never overlapping, is an invariant. These conditions and the failure policy can guide the implementation request, tests and review.

Consider this release function from a slot-pool example written for this article. The used flag records occupancy; next_free holds the index of the next empty slot.

static bool pool_release(Pool *p, size_t i)
{
    if (i >= CAPACITY || !p->slots[i].used) return false;
    Slot *s = &p->slots[i];
    s->used = false;
    s->links.next_free = p->free_head;
    p->free_head = i;
    --p->used_count;
    return true;
}

It checks the index and rejects an already released slot. But it does not check whether the slot has been unlinked from the tree or whether another operation still holds a reference. The caller must establish those conditions. Local checks alone do not establish that release is safe.

Download the complete C example, including initialization, acquisition, release and checks, and run:

cc -std=c11 -Wall -Wextra -Werror -O2 slot_pool.c -o slot_pool
./slot_pool
full pool rejected; slot 1 reused
slot ownership checks passed

The example targets a current C11 compiler and runs in one process and one thread. It checks four-slot exhaustion and reuse, invalid indices, immediate double release, and slot ownership during repeated operations. Its test scope ends at slot management. It does not implement the tree or attach shared memory.

Follow references beyond the function boundary

The node containing the requested key may differ from the node physically removed by tree deletion. An implementation that copies a successor’s data when deleting a node with two children is one example. The pool must receive the slot that the particular erase implementation actually unlinks.

Outstanding references may still remain. Suppose operation A keeps a pointer to slot 3 while operation B releases and reuses it. A can now read a different transaction at the same address. The access is incorrect even though the memory was never deallocated.

A free slot is reserved, initialized and linked; deletion unlinks it and waits for references to end before returning it to the free list
Review extends beyond the release function to its callers and the end of outstanding references.

The example’s used flag cannot prevent this: it becomes true again after reuse. Review needs to cover lock scope, deferred return until references end, or handles that combine a slot index with a generation number. Generation numbers alone do not eliminate a race between validation and access.

An agent review can trace where returned pointers are retained, when locks are released, and which paths can reuse the storage.

Derive tests from the operating conditions

A fresh version of this design would need the following checks in addition to slot-pool unit tests:

ConditionBehavior to verify
Different attachment addressesProcesses resolve the same entry from their own segment base
Concurrent lookup, deletion and reuseStorage is not reused while a reference remains valid
Exhaustion or partial failureAgreed failure behavior occurs without leaking reservations
Unexpected process exitInterrupted setup or mutation follows the recovery policy
Target compilerSyntax, alignment and types build and run in the supported environment

The first check matters because attachment addresses can differ between processes. Sharing raw pointers requires address assumptions; offsets or slot indices can instead be resolved against each process’s segment base. The original implementation used pointers and the teaching example uses indices, so their address handling should not be treated as equivalent.

Interprocess synchronization requires appropriate settings, such as the distinction between process-private POSIX mutexes and the shared attribute PTHREAD_PROCESS_SHARED. That setting alone does not provide recovery from an interrupted update.

Performance needs a separate comparison of throughput, latency distributions and memory under equivalent inputs, occupancy and synchronization. Array storage does not make tree traversal contiguous, and reuse does not shrink reserved capacity. Passing the slot-pool checks cannot establish a performance gain.

Turn the lesson into a task brief

For a new task based on this example, a brief for the agent could look like this:

Goal
Several processes look up and update shared state by key.

Known constraints
- Manage storage inside a preallocated shared region.
- Account for process-specific attachment addresses.
- Use only features supported by the designated compiler.

Decisions still needed
- Capacity, key characteristics, and lookup/update ratio
- Whether ordering or range queries are required
- Duplicate-key, exhaustion, and process-exit policies

Review request
- Compare indexing and storage-management choices separately.
- Define ownership, synchronization scope, and invariants.
- Map each condition to a check; identify unresolved assumptions.
- Implement the agreed scope and report evidence against it.

Some entries can start empty. Fill them in through repository inspection with the agent, and narrow alternatives through small experiments.

Keep the reasons alongside the code

Revisiting the old code meant reconstructing part of the reason for choosing a red-black tree. The implementation survived, but the alternatives considered and the reasons for rejecting them were not fully recorded.

When working with an AI agent, keep a short record of constraints → decisions → rationale → verification → unresolved assumptions alongside the code. Someone later replacing the free list with a general-purpose allocator, or narrowing a lock, can then see what must be checked again.

Agent-assisted changes still need evidence that they preserve the original conditions. Keeping that evidence alongside the implementation carries the design intent into the next change and review.

References

Further Reading