Skip to content

Commit d29c1ff

Browse files
str + vec: Insert/Push family + stack-init scopes (no allocator)
Stack-init scopes (StrInitStack, VecInitStack) drop their allocator argument -- NULL allocator is the runtime signal for "non-growable". validate_vec / vec_aligned_size / VecAlignedOffsetAt tolerate it; reserve_vec / deinit_vec / reduce_space_vec abort with the dedicated "vector not growable, no allocator assigned, probably stack inited" message. VecInitStack takes the element type as its first arg so the macro mints `name` itself (matches StrInitStack). _Alignas(T) on the backing struct field guarantees slot alignment for typed Vecs. IterInitFromVec* fall back to alignment 1 on NULL allocator so a StrIter over a stack-init Str works. No deep-copy variant: deep-copy callbacks need an allocator, so heap-backed Vec is the answer. Str Insert family settles on the unified L/R + *Many shape: StrInsert/L/R single-char, StrInsertMany(Zstr) and (Zstr, len) carry the range companion; PushBack and PushFront follow the same shape; StrMerge defaults to L per the unsuffixed-default-is-L convention. Migrations: Sys/Proc.c::sys_proc_read_internal now stages reads in a stack Str + StrMergeR (the old char tmpbuf[1024] + StrPushBackMany smell). Sys/Dns.c parsers rewritten to StrIter, matching the Parsers/JSON.c / Parsers/KvConfig.c precedent. dns_resolve_5_zstr / dns_resolve_4_vec_zstr go StrInitStack end-to-end. Sys/Dir.c and Log.h::LOG_SYS_* converted to the new scope shape. Bug fixes: Io.c::write_char_internal was producing `\x\HH` instead of `\xHH` -- a stray backslash from `cs[3] = {'\\', c1, c2}` after `\\x` was already emitted. Now matches the sibling pattern in the byte-loop above. DnsResolverDeinit tolerates a zero-init or partially-failed-init struct (validate_vec would abort on the zero-magic vec otherwise). Conventions: CODING-CONVENTIONS.md grows a dedicated "Stack-init APIs" section. VecCopyInit / VecCopyDeinit accessors added so tests do not reach into Vec internals.
1 parent 28073d0 commit d29c1ff

54 files changed

Lines changed: 1715 additions & 1221 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Bin/Beam.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ static void log_request_summary(Allocator *alloc, Zstr client_addr, Zstr prefix_
249249
Scope(scope, DefaultAllocator) {
250250
(void)alloc;
251251
Str raw = StrInit(scope);
252-
StrPushBackZstr(&raw, prefix_bytes);
252+
StrPushBackMany(&raw, prefix_bytes);
253253

254254
HttpRequest req = HttpRequestInit(scope);
255255
Zstr end = HttpRequestParse(&req, (Zstr)StrBegin(&raw));

CODING-CONVENTIONS.md

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,28 @@ of the codebase to see them in action.
8080
In both cases the zero-on-take invariant holds on success AND failure.
8181
- **R-form (`*FromMemoryCopy` / `VecInsertR`)** copies; the caller's data
8282
is untouched and remains theirs.
83+
- **Unsuffixed default = L.** When a family has both `*L` and `*R`
84+
variants, the bare unsuffixed name aliases to L: `VecInsert`
85+
`VecInsertL`, `ElfOpenFromMemory` is the L-form (`ElfOpenFromMemoryCopy`
86+
is the explicit R counterpart). Don't dispatch an unsuffixed macro to
87+
an R primitive — that inverts the convention and surprises callers.
88+
- **L primitives are strict-typed; R primitives are permissive-typed.**
89+
L's internal `CHECK_TYPE_EQUIVALENCE` requires the source element
90+
type to match the container's element type *exactly* (no const
91+
promotion, no integer narrowing) — because L zeroes the source on
92+
success, and that's only safe on writeable storage of the right
93+
element type. R uses `CHECK_TYPE_CONVERTIBLE`, which accepts any
94+
initialization-compatible source (e.g. `int` literal → `char`,
95+
`char *``const char *`). Two consequences:
96+
- Read-only / const sources (`Zstr`, `Cstr`-shaped `(Zstr, size)`)
97+
*cannot* ride an L-form macro. The L primitive's strict-type
98+
check rejects `const T *` for a `Vec(T)`. So a family whose
99+
inputs are only ever const surfaces only R-form variants; there
100+
is no L-form and no unsuffixed default — callers spell out `*R`.
101+
- Integer literals like `'-'` (type `int` in C11) compile with R's
102+
`CHECK_TYPE_CONVERTIBLE` but fail L's `CHECK_TYPE_EQUIVALENCE`.
103+
Callers who want L semantics with literals must bind to a typed
104+
lvalue first (`char c = '-'; VecInsertL(&v, c, 0)`).
83105
- **Prefer `Str` to `Zstr`.** `Str` carries its length and allocator
84106
inline; `Zstr` is a raw NUL-terminated pointer. For stored fields,
85107
parameters the function needs a length for, and anywhere the caller's
@@ -166,9 +188,56 @@ of the codebase to see them in action.
166188
- **Allocators fail loud.** Bad free, foreign pointer, state-machine
167189
violation → `LOG_FATAL` with a backtrace. Soft no-op returns hide bugs;
168190
this project would rather you crash on the spot.
169-
- **Stack-promote transient strings** with `StrInitStack(str, alloc, n,
170-
body)` and a rough capacity. Don't spin up a fresh `HeapAllocator` to
171-
hold a short-lived string.
191+
- **Stack-promote transient containers** with `*InitStack` (`StrInitStack`,
192+
`VecInitStack`, ...) -- see the dedicated section below.
193+
194+
## Stack-init APIs
195+
196+
- **Stack means stack.** `*InitStack` macros take **no allocator**.
197+
The backing storage is a stack array sized by the `ne` argument;
198+
the container's inline allocator slot is `NULL`. If you would have
199+
written `char buf[N]` and tracked its length yourself, use
200+
`StrInitStack(name, N) { ... }` instead -- the lifetime, capacity,
201+
and zero-on-exit are all expressed by the macro.
202+
- **Overflow is a contract violation, not a fallback.** Any
203+
operation that would grow the container past `ne` lands in the
204+
realloc path with a NULL allocator; the runtime aborts with
205+
`LOG_FATAL("vector not growable, no allocator assigned, probably
206+
stack inited")`. There is no spill / overflow / upstream allocator
207+
argument. If a caller legitimately needs spill behaviour, that
208+
caller wants an allocator-backed container, not `*InitStack`.
209+
- **No deep-copy callbacks on stack-init.** `copy_init` / `copy_deinit`
210+
hooks take an `Allocator *` for the inner resources they own. A
211+
stack-init container has no allocator and so can't pair with deep
212+
copies. If you need deep-copy ownership for elements, switch to a
213+
heap-backed container -- there is no `*InitStackWithDeepCopy`.
214+
- **For-chain scope idiom.** The body is regular code below the
215+
macro, not a brace-delimited macro argument. This keeps `return` /
216+
`break` / `continue` from being macro-mangled and matches `Scope(...)`
217+
in `<Misra/Std/Allocator.h>`:
218+
219+
StrInitStack(buf, 1024) {
220+
ssize_t n = read(fd, StrBegin(&buf), 1023);
221+
StrResize(&buf, (size)n);
222+
StrMergeR(out, &buf);
223+
}
224+
225+
`return` / `goto` leaving the body skip the zero-on-exit cleanup
226+
(a C-level limitation, not a bug). Use `break` to leave cleanly.
227+
- **When to use vs heap container.** Reach for `*InitStack` when the
228+
size is bounded by something you control (a syscall buffer, a
229+
formatted line, a per-iteration scratch space). Reach for a heap
230+
container when the size is caller-controlled or unbounded
231+
(parsing arbitrary input, accumulating across iterations whose
232+
total is unknown). Don't paper over an unknown-size case with a
233+
guessed `ne`.
234+
- **Don't call `Deinit` on a stack-backed handle inside the scope.**
235+
The scope macro zeroes both the backing buffer and the container
236+
handle on exit, which invalidates the magic value; after the
237+
scope, any operation on the handle (including a stray
238+
`StrDeinit` / `VecDeinit`) trips `validate_vec` with the
239+
`Either uninitialized or corrupted!` message. Inside the scope,
240+
there is no separate teardown to run -- the macro IS the teardown.
172241

173242
## Libc-free mindset
174243

@@ -229,6 +298,20 @@ of the codebase to see them in action.
229298
(`<Misra/Std/Container/Str/Access.h>`), and the corresponding `Vec`
230299
ones. Direct field access is reserved for the container's own
231300
implementation.
301+
- **`_Generic` arm bodies must have uniform shape.** C11 type-checks
302+
*every* arm's body (not just the selected one). An arm that
303+
dereferences (`((T *)(val))->field`) and an arm that treats `val`
304+
as a value (`(char)(val)`) cannot coexist in the same `_Generic`:
305+
when `val` has the *other* arm's type, the non-selected arm fails
306+
to type-check (CHECK_TYPE_EQUIVALENCE / CHECK_TYPE_CONVERTIBLE
307+
fires inside the unreached arm's macro expansion). Concretely:
308+
do not mix single-value arms (`char:`, `int:`) with pointer-deref
309+
arms (`Str *:`, `Zstr:`) in one `_Generic`. Split into two macros
310+
— one for the single-value shape, one for the range / pointer
311+
shape — and give them distinct names (e.g. `*Insert` for single,
312+
`*InsertMany` for range). The unified-macro-with-mixed-shapes
313+
pattern only works when every arm body has the same pointer-deref
314+
or pointer-cast shape.
232315
233316
## Macro hygiene
234317

Fuzz/Harness/Str.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ void fuzz_str(Str *str, StrFunction func, const uint8_t *data, size_t *offset, s
193193
if (VecLen(str) > 0 && *offset + 1 <= size) {
194194
char *cstr = generate_cstring(data, offset, size, 20);
195195
if (cstr) {
196-
int result = StrCmpCstr(str, cstr, strlen(cstr));
196+
int result = StrCmp(str, cstr, strlen(cstr));
197197
(void)result; // Suppress unused variable warning
198198
free(cstr);
199199
}
@@ -205,7 +205,7 @@ void fuzz_str(Str *str, StrFunction func, const uint8_t *data, size_t *offset, s
205205
if (VecLen(str) > 0 && *offset + 1 <= size) {
206206
char *zstr = generate_cstring(data, offset, size, 20);
207207
if (zstr) {
208-
int result = StrCmpZstr(str, zstr);
208+
int result = StrCmp(str, zstr);
209209
(void)result; // Suppress unused variable warning
210210
free(zstr);
211211
}
@@ -216,7 +216,7 @@ void fuzz_str(Str *str, StrFunction func, const uint8_t *data, size_t *offset, s
216216
case STR_FIND_STR : {
217217
if (VecLen(str) > 0) {
218218
Str temp = generate_str_from_input(data, offset, size, 10, alloc);
219-
char *found = StrFindStr(str, &temp);
219+
char *found = StrFind(str, &temp);
220220
(void)found; // Suppress unused variable warning
221221
StrDeinit(&temp);
222222
}
@@ -227,7 +227,7 @@ void fuzz_str(Str *str, StrFunction func, const uint8_t *data, size_t *offset, s
227227
if (VecLen(str) > 0 && *offset + 1 <= size) {
228228
char *zstr = generate_cstring(data, offset, size, 10);
229229
if (zstr) {
230-
char *found = StrFindZstr(str, zstr);
230+
char *found = StrFind(str, zstr);
231231
(void)found; // Suppress unused variable warning
232232
free(zstr);
233233
}
@@ -239,7 +239,7 @@ void fuzz_str(Str *str, StrFunction func, const uint8_t *data, size_t *offset, s
239239
if (VecLen(str) > 0 && *offset + 1 <= size) {
240240
char *cstr = generate_cstring(data, offset, size, 10);
241241
if (cstr) {
242-
char *found = StrFindCstr(str, cstr, strlen(cstr));
242+
char *found = StrFind(str, cstr, strlen(cstr));
243243
(void)found; // Suppress unused variable warning
244244
free(cstr);
245245
}

0 commit comments

Comments
 (0)