Comment by stevendgarcia
3 days ago
Your comment got my wheels turning so.. quick followup.
Since you lived with this for such a long stretch, I'd love your gut reaction to the specific escape hatches I'm building in to avoid the rigidity trap:
1. Arenas grow, not fixed:
Unlike stack frames, the arenas in my model can expand dynamically. So it's not "size for worst case"—it's "grow as needed, free all at once when scope ends." A request handler that processes 10 items or 10,000 items uses the same code; the arena just grows.
2. Handles for non-hierarchical references
When data genuinely needs to outlive its lexical scope or be shared across the hierarchy, you get a generational handle:
let handle = app_cache.store(expensive_result)
// handle can be passed around, stored, retrieved later
// data lives in app scope, not request scope
The handle includes a generation counter, so if the underlying scope dies, dereferencing returns None instead of use-after-free.
3. Explicit clone for escape:
If you need to return data from an inner scope to an outer one, you say `clone()` and it copies to the caller's arena. Not automatic, but not forbidden either.
4. The hierarchy matches server reality:
App (config, pools, caches)
└── Worker (thread-local state)
└── Task (single request)
└── Frame (loop iteration)
For request/response workloads, this isn't an artificial constraint—it's how the work actually flows. The memory model just makes it explicit.
Where I think it still gets awkward:
* Graph structures with cycles (need handles, less ergonomic than GC)
* FFI with libraries expecting malloc/free (planning an `unmanaged` escape hatch)
* Long-running mutations without periodic scope resets (working on incremental reclamation)
Do you think this might address the pain you experienced, or am I missing something? Particularly curious whether the handle mechanism would have helped with the cases where you had to hammer code into the hierarchy.
There is a lot in what you describe that goes substantially beyond what I had in that codebase. It was basically a set of idioms with some helper code for a few common functions. Having an opinionated, predefined hierarchy is a good approach - there were concepts similar to your app/worker/task in the codebase I dealt with, although the equivalent of worker and task were both (kind of, its been a few years) situated below app.
In the code I mentioned, a lot of use was made of multi-level arrays of structs, with functions being passed a pointer to a root data structure and one or more array indexes. This made function argument validation somewhat better than just checking for null pointers, as array sizes were mostly stored in their containing struct or were constant. I don't know if that corresponds to your 'handle' concept, but I suspect you're doing something more general-purpose.
There were simple reader/writer functions for DTOs (which were mostly stored in arrays) but no idea of an ORM.
Escaping using clone seems sound. The ability to expand scopes seems (if I understand it) powerful, but perhaps makes reasoning about the dynamic behaviour of the code harder. Having some kind of observability around this may help.
Refactoring wasn't a huge problem. The codebase was basically a statically-linked monolith, so dependencies were simplified. I think that having an explicit way to indicate architecture boundaries might be useful.
Overall, I suspect that if there are limitations with your approach then it may be that, while it simplifies 80-90% of a problem, the remainder is hard to fit into the architectural framework. Dogfooding some production-level applications should help.
Good luck. What you're doing is fascinating, and I hope you'll update HN with your progress.