Files
libakstdlib/include/akstdlib.h

506 lines
28 KiB
C
Raw Normal View History

#ifndef _AKSTDLIB_H_
#define _AKSTDLIB_H_
#include <akerror.h>
Upgrade to libakerror 1.0.0 Bump deps/libakerror 22 commits to 5ff8790 (1.0.0), which makes the status-name table private, moves consumer status codes to a band starting at AKERR_FIRST_CONSUMER_STATUS, enforces range ownership rather than treating it as advisory, and gives the library an soname. See deps/libakerror/UPGRADING.md. src/stdlib.c needed no changes. This library defines no status codes of its own -- it raises libakerror's AKERR_* codes and propagates errno, both inside libakerror's reserved 0-255 band -- and it never referenced AKERR_MAX_ERR_VALUE, __AKERR_ERROR_NAMES, AKERR_STATUS_RANGE_OK or AKERR_STATUS_NAME_OK. What moved was everything around the code: A -DAKSL_COVERAGE=ON build stopped configuring at all. libakerror namespaces its `mutation` target when embedded but not its `coverage` target, so it collided with ours. Shadow add_custom_target for the duration of the add_subdirectory() call and rename the dependency's to akerror_coverage, alongside the existing add_test shadow. Fix upstream and delete the workaround; recorded in TODO.md. Pin the 1.0.0 floor three ways, since no single one covers every consumption path: an #error in akstdlib.h feature-testing AKERR_FIRST_CONSUMER_STATUS, because libakerror publishes no version macro; Requires: akerror >= 1.0.0 in akstdlib.pc, which also gets consumers -lakerror transitively; and find_dependency(akerror) in akstdlibConfig.cmake. The last was already broken before this bump -- the template still carried its MyLibraryConfig placeholder with the dependency commented out, so any external find_package(akstdlib) failed with a bare "akerror::akerror not found" out of the generated targets file. Branch coverage of src/stdlib.c fell from 51.0% to 44.3% with no source or test change: the 1.0.0 PREPARE_ERROR/FAIL_* macros expand to more branches at every call site, so 337/661 became 481/1087 -- 144 more branches covered, 426 more counted. Line coverage held at 99.0% (200/202) and function coverage at 100% (21/21). Re-ratchet the CI branch gate 45 -> 40 rather than chase branches that belong to libakerror's own suite. tests/test_status_registry.c pins the contract that made the status-code migration a no-op: libakstdlib reserves no consumer range, so an application may allocate from AKERR_FIRST_CONSUMER_STATUS without coordinating with it, and every status this library raises is inside the reserved band with a name actually registered -- an unnamed one degrades to "Unknown Error" in every later stack trace, which nothing else would notice. It exercises the new ownership enforcement too, so the "reserves nothing" assertion cannot pass vacuously. ctest 13/13, ASan+UBSan 13/13, coverage 15/15 at 90/40, mutation 89.6% (155/173, unchanged). Also verified out of tree: the #error fires as the first diagnostic against a stale akerror.h, pkg-config refuses akerror 0.9.0, and an external find_package(akstdlib) consumer builds and runs against a temp-prefix install. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:20:54 -04:00
/*
* libakerror 1.0.0 is the floor. That release moved the status-name table into a
* private registry -- AKERR_MAX_ERR_VALUE and __AKERR_ERROR_NAMES are gone, the
* registry entry points raise akerr_ErrorContext * instead of returning int, and
* the library gained an soname -- so a translation unit that pairs this header
* with a pre-1.0.0 akerror.h is an ABI mismatch, not just a compile problem.
*
* libakerror publishes no version macro, so this feature-tests on
* AKERR_FIRST_CONSUMER_STATUS, which that release introduced, rather than on a
* version number that does not exist. Without the guard a stale installed header
* fails much further in, as a pile of unrelated errors inside src/stdlib.c.
*
* See deps/libakerror/UPGRADING.md.
*/
#ifndef AKERR_FIRST_CONSUMER_STATUS
#error "libakstdlib requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
#endif
Version the library at 0.1.0 project() now carries VERSION 0.1.0, and is the single place a version number is spelled. It flows into generated version macros, the shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried no soname at all. 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose fixes change documented behaviour, so the API is not being promised yet. While the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt and the #if in tests/test_version.c encode that rule and are tested against each other. include/akstdlib_version.h.in is configured into the build tree as akstdlib_version.h and installed beside akstdlib.h. It defines AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME. AKSL_VERSION_NUMBER is computed rather than written as a literal, because a literal 000100 is octal in C and would make 0.1.0 compare as 64; test_version.c asserts it against the runtime components, so a rewrite to a literal fails. Those macros record what a caller was compiled against. aksl_version(), aksl_version_string() and aksl_version_soname() report what actually loaded, and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both. It is a macro so that it expands at the caller's site and captures the caller's numbers; the function compares them against the ones baked into the library. Compatibility is "same soname", so patch is ignored -- a caller built against 0.1.0 keeps working against 0.1.7. Normally the soname catches a mismatch at load time and the check never fires. It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under the 0.1 filename loads happily, and only the check notices. write_basic_package_version_file() uses SameMinorVersion to mirror the soname, falling back to ExactVersion below CMake 3.11 where that mode does not exist. The fallback is stricter than the soname rule -- it pins the patch level too -- but never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad one. Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches and 25/25 functions. That puts branch coverage back over the old 45 gate, but the gate stays at 40: 0.1 points of headroom is not a ratchet. ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config --modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and 1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0 dropped in under the 0.1 filename caught by it, and an embedded add_subdirectory build keeping its own version rather than the parent's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:40:49 -04:00
/*
* AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
* Generated by CMake from include/akstdlib_version.h.in, which is why there is
* no such file in the source tree -- it is configured into the build tree and
* installed alongside this header.
*/
#include <akstdlib_version.h>
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
/*
* What this header needs in its own declarations, and no more:
* stdio.h FILE
* stddef.h size_t
* stdint.h uint32_t
* stdarg.h va_list, for the v* forms of the formatted-output wrappers
*
* It used to pull in stdlib.h and string.h as well, which nothing here needs and
* which every consumer then got whether it wanted them or not. stddef.h in place
* of stdlib.h is the same size_t at a fraction of the namespace. TODO.md 2.2.16.
*/
#include <stdarg.h>
#include <stddef.h>
2026-06-02 17:11:38 -04:00
#include <stdint.h>
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#include <stdio.h>
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */
#include <sys/types.h>
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#ifdef __cplusplus
extern "C" {
#endif
/*
* Restore the compile-time format/argument checking that callers otherwise lose
* by going through a variadic wrapper: without it, printf("%d", "str") is caught
* and aksl_printf(&n, "%d", "str") is not. TODO.md 2.2.5.
*/
#if defined(__GNUC__) || defined(__clang__)
#define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg) \
__attribute__((format(printf, __fmt_index, __first_arg)))
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
#define AKSL_SCANF_FORMAT(__fmt_index, __first_arg) \
__attribute__((format(scanf, __fmt_index, __first_arg)))
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#else
#define AKSL_PRINTF_FORMAT(__fmt_index, __first_arg)
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
#define AKSL_SCANF_FORMAT(__fmt_index, __first_arg)
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#endif
typedef struct aksl_ListNode {
void *data;
struct aksl_ListNode *next;
struct aksl_ListNode *prev;
} aksl_ListNode;
typedef struct aksl_TreeNode {
struct aksl_TreeNode *parent;
struct aksl_TreeNode *left;
struct aksl_TreeNode *right;
void *leaf;
} aksl_TreeNode;
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#define AKSL_TREE_SEARCH_BFS 0 /** Breadth-first (left child before right) search mode for tree nodes */
#define AKSL_TREE_SEARCH_BFS_RIGHT 1 /** Breadth-first, right child before left, search mode for tree nodes */
#define AKSL_TREE_SEARCH_DFS 2 /** Alias for AKSL_TREE_SEARCH_DFS_PREORDER */
#define AKSL_TREE_SEARCH_DFS_PREORDER 2 /** Depth first pre-order (root, left, right) search mode for tree nodes */
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#define AKSL_TREE_SEARCH_DFS_INORDER 3 /** Depth first in-order (left, root, right) search mode for tree nodes */
#define AKSL_TREE_SEARCH_DFS_POSTORDER 4 /** Depth first post-order (left, right, root) search mode for tree nodes */
#define AKSL_TREE_SEARCH_VISIT 5 /** Visit the node and stop; do not traverse the children */
/*
* How deep aksl_tree_iterate will walk before raising AKERR_OUTOFBOUNDS.
*
* This is a real limit, not a formality: the depth-first walk is recursive, so
* without it a degenerate chain overflows the stack and a child pointing back at
* an ancestor recurses until the process dies. 256 is far past any balanced tree
* that fits in memory (2^256 nodes) and comfortably short of a stack overflow,
* so in practice it only ever rejects the degenerate shapes it exists to reject.
*
* It also sizes the ancestor buffer aksl_tree_iterate keeps on its own stack --
* one pointer per level, so raising it costs 8 bytes of stack per level.
*/
#define AKSL_TREE_MAX_DEPTH 256
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/*
* Head/tail/length container. aksl_list_append has to walk the list to find its
* tail, so building n nodes with it is O(n^2); the container makes that O(1) and
* makes the length free. It owns no memory -- the nodes are still the caller's.
*/
typedef struct aksl_List {
aksl_ListNode *head;
aksl_ListNode *tail;
size_t length;
} aksl_List;
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodeIterator)(aksl_ListNode *node, void *data);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNode *node, void *data);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr);
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/* Reports its answer through *matched (non-zero to accept) and may raise. */
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_ListNodePredicate)(aksl_ListNode *node, void *data, int *matched);
/* Negative, zero or positive through *dest, as strcmp(3) has it. */
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeCompareFunc)(void *a, void *b, int *dest);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_HashMapIterator)(const char *key, void *value, void *data);
Version the library at 0.1.0 project() now carries VERSION 0.1.0, and is the single place a version number is spelled. It flows into generated version macros, the shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried no soname at all. 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose fixes change documented behaviour, so the API is not being promised yet. While the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt and the #if in tests/test_version.c encode that rule and are tested against each other. include/akstdlib_version.h.in is configured into the build tree as akstdlib_version.h and installed beside akstdlib.h. It defines AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME. AKSL_VERSION_NUMBER is computed rather than written as a literal, because a literal 000100 is octal in C and would make 0.1.0 compare as 64; test_version.c asserts it against the runtime components, so a rewrite to a literal fails. Those macros record what a caller was compiled against. aksl_version(), aksl_version_string() and aksl_version_soname() report what actually loaded, and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both. It is a macro so that it expands at the caller's site and captures the caller's numbers; the function compares them against the ones baked into the library. Compatibility is "same soname", so patch is ignored -- a caller built against 0.1.0 keeps working against 0.1.7. Normally the soname catches a mismatch at load time and the check never fires. It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under the 0.1 filename loads happily, and only the check notices. write_basic_package_version_file() uses SameMinorVersion to mirror the soname, falling back to ExactVersion below CMake 3.11 where that mode does not exist. The fallback is stricter than the soname rule -- it pins the patch level too -- but never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad one. Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches and 25/25 functions. That puts branch coverage back over the old 45 gate, but the gate stays at 40: 0.1 points of headroom is not a ratchet. ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config --modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and 1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0 dropped in under the 0.1 filename caught by it, and an embedded add_subdirectory build keeping its own version rather than the parent's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 22:40:49 -04:00
/*
* Version of the shared library actually loaded, as opposed to the
* AKSL_VERSION_* macros above, which record what the caller was *compiled*
* against. The two differ exactly when a stale libakstdlib.so is on the loader
* path, which is the failure these entry points exist to name.
*
* All three pointers are required; this library treats a NULL out-param as a
* caller error rather than as "don't care" (so does aksl_free).
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch);
/*
* The loaded library's version as "MAJOR.MINOR.PATCH", and its soname. Neither
* can fail -- they return pointers to string literals in the library's own
* .rodata -- so they return the string directly rather than an error context,
* the same exception akerr_name_for_status() makes for the same reason.
*/
const char *aksl_version_string(void);
const char *aksl_version_soname(void);
/*
* Compare the caller's compiled-in version against the loaded library's.
* Compatibility is defined as "same soname", so pre-1.0 both major and minor
* must match and patch is ignored; from 1.0 only major will matter. Raises
* AKERR_VALUE naming both versions on a mismatch.
*
* Call it through AKSL_VERSION_CHECK() below rather than directly. The macro
* expands at *your* call site, so it captures the AKSL_VERSION_* the caller was
* built with; the function compares them against the values baked into the
* library. Passing the numbers by hand defeats the entire mechanism.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch);
#define AKSL_VERSION_CHECK() \
aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH)
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
/*
* Stream I/O.
*
* fread and fwrite take a required size_t *nmemb_out. It is always written --
* including on the failure paths -- so a caller who gets AKERR_EOF can find out
* how much data arrived before the stream ran out, which is the whole point of
* distinguishing EOF from an error. A transfer that comes up short with no
* stream error set is AKERR_IO, not silent success.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(const char *pathname, const char *mode, FILE **fp);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream, size_t *nmemb_out);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp, size_t *nmemb_out);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fclose(FILE *stream);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
/*
* Memory. aksl_malloc and aksl_calloc reject a zero-size request outright rather
* than reporting whatever errno happened to hold when malloc(0) returned NULL.
* aksl_realloc takes the pointer by reference and leaves it valid and untouched
* on failure, which is realloc(3)'s classic leak closed off.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_calloc(size_t nmemb, size_t size, void **dst);
akerr_ErrorContext AKERR_NOIGNORE *aksl_realloc(void **ptr, size_t size);
akerr_ErrorContext AKERR_NOIGNORE *aksl_free(void *ptr);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_freep(void **ptr);
akerr_ErrorContext AKERR_NOIGNORE *aksl_memset(void *s, int c, size_t n);
/* Overlapping ranges are AKERR_VALUE, not undefined behaviour; use aksl_memmove. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcpy(void *d, const void *s, size_t n);
akerr_ErrorContext AKERR_NOIGNORE *aksl_memmove(void *d, const void *s, size_t n);
akerr_ErrorContext AKERR_NOIGNORE *aksl_memcmp(const void *a, const void *b, size_t n, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_memchr(const void *s, int c, size_t n, void **dest);
/*
* Formatted output. *count is the byte count written excluding the terminating
* NUL, and is 0 on every failure path.
*
* There is no aksl_sprintf. It wrapped vsprintf, which cannot be bounded, and an
* error-handling wrapper around an unbounded write is the sharp edge this
* library exists to remove. aksl_snprintf replaces it and treats truncation as
* AKERR_OUTOFBOUNDS rather than as a short success.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_printf(int *count, const char *restrict format, ...) AKSL_PRINTF_FORMAT(2, 3);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fprintf(int *count, FILE *restrict stream, const char *restrict format, ...) AKSL_PRINTF_FORMAT(3, 4);
akerr_ErrorContext AKERR_NOIGNORE *aksl_snprintf(int *count, char *restrict str, size_t size, const char *restrict format, ...) AKSL_PRINTF_FORMAT(4, 5);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_vprintf(int *count, const char *restrict format, va_list args);
akerr_ErrorContext AKERR_NOIGNORE *aksl_vfprintf(int *count, FILE *restrict stream, const char *restrict format, va_list args);
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsnprintf(int *count, char *restrict str, size_t size, const char *restrict format, va_list args);
/*
* String to number.
*
* These are strict, unlike the libc functions they are named for:
*
* no digits consumed -> AKERR_VALUE
* trailing junk, endptr NULL -> AKERR_VALUE
* value outside the type -> ERANGE
*
* and *dest is 0 on every failure. Pass a non-NULL endptr to opt out of the
* trailing-junk check and read a number off the front of a longer string.
*
* The ato* forms are aksl_strto* with base 10 and a NULL endptr, so the whole
* string must be a number. "0x10" is AKERR_VALUE through aksl_atoi -- base 10
* stops at the 'x' -- and 16 through aksl_strtol(nptr, NULL, 0, &dest).
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtol(const char *nptr, char **endptr, int base, long *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoll(const char *nptr, char **endptr, int base, long long *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoul(const char *nptr, char **endptr, int base, unsigned long *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtoull(const char *nptr, char **endptr, int base, unsigned long long *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtod(const char *nptr, char **endptr, double *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtof(const char *nptr, char **endptr, float *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtold(const char *nptr, char **endptr, long double *dest);
2026-05-24 09:52:24 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_atoi(const char *nptr, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_atol(const char *nptr, long *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_atoll(const char *nptr, long long *dest);
2026-05-25 21:29:46 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_atof(const char *nptr, double *dest);
2026-05-24 09:52:24 -04:00
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
/*
* realpath(3). `buflen` must be at least PATH_MAX, because realpath(3) offers no
* way to bound its own write -- an undersized buffer is AKERR_OUTOFBOUNDS rather
* than a buffer overflow. aksl_realpath_alloc sizes and allocates the result
* instead; *dest is then the caller's to release with aksl_free.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath(const char *restrict path, char *restrict resolved_path, size_t buflen);
akerr_ErrorContext AKERR_NOIGNORE *aksl_realpath_alloc(const char *restrict path, char **dest);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
/*
* djb2. Length-driven, so embedded NUL bytes hash like any other byte; bytes
* >= 0x80 are read as unsigned, so the result matches canonical djb2 and does
* not depend on whether plain char is signed on the target.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2(const char *str, size_t len, uint32_t *hashval);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32_t *hashval);
2026-06-02 17:11:38 -04:00
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/* ---------------------------------------------------------------------- */
/* Strings -- src/string.c */
/* ---------------------------------------------------------------------- */
/*
* Two conventions run through this whole section.
*
* The copying and concatenating functions take the size of the destination
* buffer, even the ones named after libc functions that do not. strcpy(3) and
* strcat(3) cannot be called safely without knowing how much room there is, so
* a wrapper that took the same arguments would be an error-handling wrapper
* around a buffer overflow. dstsize is the whole buffer including the
* terminator -- pair `char buf[64]` with `sizeof(buf)`. Truncation is
* AKERR_OUTOFBOUNDS, and nothing is written when it happens, so ignoring the
* status leaves an empty string rather than a plausible-looking prefix.
*
* The searching functions answer through an out-param, and finding nothing is a
* successful answer of NULL rather than an error. The result points into the
* caller's own string and must not be freed.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src);
/* Always terminates, and never NUL-pads to n; strncpy(3) does both the other way. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n);
/* *dest is the caller's, to release with aksl_free or aksl_freep. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest);
/*
* The reentrant tokeniser only. strtok(3) keeps its state in a hidden static,
* so two interleaved tokenisations corrupt each other silently; it is not
* wrapped. Running out of tokens is success with *dest NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest);
/*
* The message for a status, into the caller's buffer. Knows this library's own
* statuses as well as errno values, which strerror_r(3) could not.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen);
/* ---------------------------------------------------------------------- */
/* Streams -- src/stream.c */
/* ---------------------------------------------------------------------- */
/*
* Where a function has a genuine "nothing more to read" outcome, that is
* AKERR_EOF rather than AKERR_IO, so a read loop can tell the end of its input
* from the failure of it. That applies to aksl_fgetc, aksl_fgets, aksl_getline,
* aksl_getdelim and aksl_fscanf.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence);
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence);
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos);
/* NULL stream means "every output stream", as in fflush(3); it is not an error. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out);
akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim, FILE *stream, size_t *len_out);
akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode, FILE *stream, FILE **fp);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp);
/*
* The scanf family takes the number of conversions the caller expects, because
* comparing scanf(3)'s return against that number by hand at every call site is
* the check everyone eventually forgets -- and forgetting it leaves the
* unassigned arguments holding whatever they held before. Anything short of
* `expected` is AKERR_VALUE. Pass 0 to opt out and read *assigned yourself.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(2, 5);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format, int expected, int *assigned, ...) AKSL_SCANF_FORMAT(2, 5);
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format, int expected, int *assigned, va_list args);
akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format, int expected, int *assigned, va_list args);
akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname);
akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath);
/*
* The template is rewritten in place, so it must be a writable buffer ending in
* six literal X characters -- a string literal is a segfault, and that is
* AKERR_VALUE here rather than a crash.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd);
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_);
// Linked list functions
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_node_init(aksl_ListNode *node, void *data);
2026-06-27 13:13:17 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_append(aksl_ListNode *list, aksl_ListNode *obj);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
/*
* `head` is required. Popping the head node has to move the caller's own head
* pointer, and there is no way to do that from a node pointer alone -- the old
* one-argument form left the caller aimed at a detached node.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_pop(aksl_ListNode **head, aksl_ListNode *node);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate(aksl_ListNode *list, aksl_ListNodeIterator iter, void *data);
// Tree functions
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_node_init(aksl_TreeNode *node, void *leaf);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_iterate(aksl_TreeNode *root, aksl_TreeNodeIterator iter, aksl_AllocFunc lalloc, aksl_FreeFunc lfree, uint8_t searchmode, void *data);
Wrap the strings, streams and collections the wishlist asked for TODO.md section 3.1's high-priority list and section 3.6's data structures. This is the surface akbasic went without: it makes 10 calls into this library and 116 to raw libc, and 69 of those 116 are strlen, strcmp, strncpy and strstr. Three new translation units, because src/stdlib.c covering four times what it did would stop being readable: src/string.c lengths, bounded copy and concatenation, duplication, comparison including the case-insensitive forms, searching, the reentrant tokenisers, and a status message that knows this library's own statuses as well as errno's. src/stream.c positioning, flushing and buffering, character and line I/O, stream state, freopen/fdopen/tmpfile, formatted input, and the file operations. src/collections.c the list functions that were missing, a head/tail container so append is O(1), a binary search tree, FNV-1a, a fixed-capacity hash map and a growable string buffer. Two conventions run through all of it. The copying functions take the destination size even where the libc function they are named for does not, because strcpy(3) cannot be called safely without it, and truncation is an error that writes nothing rather than a plausible prefix -- this is the idiom akbasic writes out by hand at ten sites. The searching functions treat "not found" as a successful answer of NULL, because absent is an answer and raising on it would make every caller handle a non-error. The hash map is akbasic's src/symtab.c generalised: open-addressed with linear probing over a caller-supplied slot array, keys copied into fixed slots so the map owns them, tombstones on delete so a removal cannot cut a probe chain, and a refusal rather than a resize when full. Tests: 16 binaries, green under the normal and sanitizer builds. The scanf wrappers take the number of conversions the caller expects, since comparing scanf(3)'s return against that by hand is the check everyone eventually forgets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:32:35 -04:00
/* ---------------------------------------------------------------------- */
/* Collections -- src/collections.c */
/* ---------------------------------------------------------------------- */
/*
* Nothing in this section allocates unless its name says so. The list and tree
* functions relink nodes the caller already owns, and the *_free_all forms take
* the free function to use -- pass NULL for aksl_free -- so a caller drawing
* from a fixed pool can hand over its own. Only aksl_strbuf_* owns memory.
*
* The bare-node list functions take the head by reference wherever the head
* itself can move, which is the case a caller doing it by hand gets wrong.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_prepend(aksl_ListNode **head, aksl_ListNode *obj);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_after(aksl_ListNode *node, aksl_ListNode *obj);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_insert_before(aksl_ListNode **head, aksl_ListNode *node, aksl_ListNode *obj);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_length(aksl_ListNode *head, size_t *dest);
/* NULL and success when nothing matches, as everywhere else in this library. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_find(aksl_ListNode *head, aksl_ListNodePredicate pred, void *data, aksl_ListNode **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_reverse(aksl_ListNode **head);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_concat(aksl_ListNode *head, aksl_ListNode *other);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_free_all(aksl_ListNode **head, aksl_FreeFunc lfree);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_iterate_reverse(aksl_ListNode *tail, aksl_ListNodeIterator iter, void *data);
/* The tracked container. push/unshift are O(1); length is a field, not a walk. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_init(aksl_List *list);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_push(aksl_List *list, aksl_ListNode *obj);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_unshift(aksl_List *list, aksl_ListNode *obj);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_remove(aksl_List *list, aksl_ListNode *node);
akerr_ErrorContext AKERR_NOIGNORE *aksl_list_clear(aksl_List *list, aksl_FreeFunc lfree);
/*
* An unbalanced binary search tree. Inserting already-sorted data gives a
* degenerate chain, which the traversal then refuses past AKSL_TREE_MAX_DEPTH --
* bounded rather than dangerous, but this is not a balanced tree and does not
* claim to be. These are the functions that set and read aksl_TreeNode.parent.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_insert(aksl_TreeNode **root, aksl_TreeNode *node, aksl_TreeCompareFunc cmp);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_find(aksl_TreeNode *root, void *leaf, aksl_TreeCompareFunc cmp, aksl_TreeNode **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_remove(aksl_TreeNode **root, aksl_TreeNode *node);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_height(aksl_TreeNode *root, int *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_count(aksl_TreeNode *root, size_t *dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_tree_free_all(aksl_TreeNode **root, aksl_FreeFunc lfree);
/*
* FNV-1a alongside djb2. It XORs then multiplies rather than multiplying then
* adding, which mixes the low bits better on short keys that share a prefix --
* which is what identifiers in a symbol table look like.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a(const char *str, size_t len, uint32_t *hashval);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_fnv1a_str(const char *str, uint32_t *hashval);
/*
* Fixed-capacity string-keyed hash map: open addressing, linear probing, the
* caller's slot array, keys copied into the slots so the map owns them.
*
* It refuses rather than resizes when full, deliberately. A table that
* reallocates is a table whose entry pointers move underneath anything holding
* one; a table that cannot grow has a worst case you can state. The cost is that
* the caller sizes it, which is why init takes the array rather than making one.
*/
#define AKSL_HASHMAP_MAX_KEY 64
#define AKSL_HASHMAP_SLOT_EMPTY 0 /** Never used; ends a probe chain */
#define AKSL_HASHMAP_SLOT_OCCUPIED 1 /** Holds a live key and value */
#define AKSL_HASHMAP_SLOT_DELETED 2 /** Tombstone; reusable, but does not end a chain */
typedef struct aksl_HashEntry {
char key[AKSL_HASHMAP_MAX_KEY];
void *value;
uint8_t state;
} aksl_HashEntry;
typedef struct aksl_HashMap {
aksl_HashEntry *slots;
size_t capacity;
size_t count;
} aksl_HashMap;
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_init(aksl_HashMap *map, aksl_HashEntry *slots, size_t capacity);
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_put(aksl_HashMap *map, const char *key, void *value);
/* A missing key is *found = 0 and success; looking and not finding is not an error. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_get(aksl_HashMap *map, const char *key, void **value, int *found);
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_remove(aksl_HashMap *map, const char *key, int *removed);
akerr_ErrorContext AKERR_NOIGNORE *aksl_hashmap_iterate(aksl_HashMap *map, aksl_HashMapIterator iter, void *data);
/*
* Growable string buffer -- the one thing here that owns memory, which is what
* makes the bounded formatting wrappers usable when the output length is not
* known in advance. Capacity doubles, so n appends cost O(n) amortised, and the
* contents are always NUL-terminated so aksl_strbuf_cstr is valid at any point.
*/
typedef struct aksl_StrBuf {
char *data;
size_t length; /** Bytes in use, terminator not counted */
size_t capacity; /** Bytes allocated, terminator included */
} aksl_StrBuf;
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_init(aksl_StrBuf *buf, size_t initial);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append(aksl_StrBuf *buf, const char *s);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_bytes(aksl_StrBuf *buf, const char *s, size_t n);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_append_char(aksl_StrBuf *buf, char c);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_appendf(aksl_StrBuf *buf, const char *format, ...) AKSL_PRINTF_FORMAT(2, 3);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_vappendf(aksl_StrBuf *buf, const char *format, va_list args);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_reset(aksl_StrBuf *buf);
/* Points into the buffer; the next append invalidates it. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_cstr(aksl_StrBuf *buf, const char **dest);
akerr_ErrorContext AKERR_NOIGNORE *aksl_strbuf_free(aksl_StrBuf *buf);
Fix the six confirmed defects and close the API contract gaps TODO.md section 2.1 recorded six defects reproduced against the built library, and section 2.2 seventeen contract gaps. Both are closed. The four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into the tests for the things they test, and that list is now empty. The defects: 2.1.1 aksl_list_append conflated Floyd cycle detection with finding the tail, so `tail` tracked the node behind the midpoint. Any append to a list of 2+ nodes silently dropped everything after it. Two separate walks now: Floyd to prove the list is finite, then a plain walk to the end. 2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor, so the whole first half of the list -- head included -- was never passed to the callback. It starts at the head. 2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame that raised it handled it and returned success, so the parent carried on into the sibling subtree. The recursion is split out and propagates the break; only the public entry swallows it. 2.1.4 va_end now matches every va_start on every path. 2.1.5 The ato* family had no error channel at all. Reimplemented over a new strto* family with errno cleared, an endptr check and a range check: AKERR_VALUE for junk, ERANGE for overflow. 2.1.6 aksl_realpath never checked resolved_path, could not be told the buffer size, and formatted an unspecified buffer with %s on its own error path. It takes a length; aksl_realpath_alloc is the allocating form. The contract gaps, in brief: errno is cleared before every wrapped call and read back through a fallback so no error can carry status 0; fopen validates pathname and mode; fread/fwrite report the transferred count through a required out-param and no longer call a short transfer a success; aksl_sprintf is gone in favour of aksl_snprintf, which treats truncation as an error; the variadic wrappers carry format attributes; djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded and implements BFS, so lalloc/lfree are used rather than merely stored; an unknown searchmode is AKERR_VALUE rather than silent success; list_pop takes the head by reference; aksl_freep, the node initialisers and extern "C" are new. Build: -pg is out of the default build (it never reached the C compiler anyway, and it is what produced the stray gmon.out), -Wall -Wextra are in, and there is a .gitignore. Tests: 11 binaries, all green under the normal and sanitizer builds. Visit-order assertions replace the step counts that could not tell the three depth-first orders apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:14:11 -04:00
#ifdef __cplusplus
}
#endif
#endif