diff --git a/include/submods/hashmap.h b/include/submods/hashmap.h new file mode 100644 index 0000000..949b554 --- /dev/null +++ b/include/submods/hashmap.h @@ -0,0 +1,179 @@ +{* +** Header for Hashmap submodule +** Include this when using EXTERNAL hashmap +** +** String-keyed hashmap with open addressing and linear probing. +** CLASS-based: multiple independent instances, each with own storage. +** DJB2 hash, power-of-2 capacity, 70% max load factor. +** +** Usage: +** #include +** DECLARE CLASS Hashmap map +** HmMake(map, HM_MEDIUM) +** HmPut$(map, "name", "Alice") +** PRINT HmGet$(map, "name") +** HmFree(map) +*} + +#ifndef HASHMAP_H +#define HASHMAP_H + +{* ============== Constants ============== *} + +' Error codes +CONST HM_SUCCESS = 0 +CONST HM_ERR_FULL = -1 +CONST HM_ERR_NOTFOUND = -2 +CONST HM_ERR_CAPACITY = -3 + +' Value type tags +CONST HmTypeStr = 0 +CONST HmTypeLng = 1 +CONST HmTypeSng = 2 +CONST HmTypeRef = 3 +CONST HmTypeBool = 4 +CONST HmTypeNull = 5 + +' Capacity presets +CONST HM_SMALL = 32 +CONST HM_MEDIUM = 128 +CONST HM_LARGE = 512 + +' String buffer sizes per element +CONST HM_KEY_SIZE = 64 +CONST HM_VAL_SIZE = 256 + +{* ============== CLASS Definition ============== *} + +CLASS Hashmap + ADDRESS keys + ADDRESS vals + ADDRESS valsL + ADDRESS types + ADDRESS status + ADDRESS order + LONGINT cap + LONGINT count + LONGINT orderCount + LONGINT cursor + LONGINT curIdx +END CLASS + +{* ============== Factory & Cleanup ============== *} + +' HmMake - Allocate backing arrays at given capacity (must be power of 2) +DECLARE SUB HmMake(Hashmap hm, LONGINT theCap&) EXTERNAL + +' HmFree - Free all ALLOC'd backing arrays +DECLARE SUB HmFree(Hashmap hm) EXTERNAL + +' HmClear - Reset all entries to empty (does not free arrays) +DECLARE SUB HmClear(Hashmap hm) EXTERNAL + +{* ============== Core Operations ============== *} + +' HmPut$ - Insert or update a string value. Returns HM_SUCCESS or HM_ERR_FULL. +DECLARE SUB SHORTINT HmPut$(Hashmap hm, theKey$, theVal$) EXTERNAL + +' HmPut& - Insert or update a LONGINT value. +DECLARE SUB SHORTINT HmPut&(Hashmap hm, theKey$, LONGINT theVal&) EXTERNAL + +' HmPut! - Insert or update a SINGLE value (raw bits stored in valsL). +DECLARE SUB SHORTINT HmPut!(Hashmap hm, theKey$, SINGLE theVal!) EXTERNAL + +' HmPutRef - Insert or update an ADDRESS reference (CLASS instance pointer). +DECLARE SUB SHORTINT HmPutRef(Hashmap hm, theKey$, ADDRESS theRef&) EXTERNAL + +' HmPutBool - Insert or update a boolean value (0 or 1). +DECLARE SUB SHORTINT HmPutBool(Hashmap hm, theKey$, SHORTINT theVal%) EXTERNAL + +' HmPutNull - Insert or update a null entry (type tag only, no value). +DECLARE SUB SHORTINT HmPutNull(Hashmap hm, theKey$) EXTERNAL + +' HmGet$ - Lookup string value by key. Returns "" if not found. +DECLARE SUB STRING HmGet$(Hashmap hm, theKey$) EXTERNAL + +' HmGet& - Lookup LONGINT value by key. Returns 0 if not found. +DECLARE SUB LONGINT HmGet&(Hashmap hm, theKey$) EXTERNAL + +' HmGet! - Lookup SINGLE value by key. Returns 0 if not found. +DECLARE SUB SINGLE HmGet!(Hashmap hm, theKey$) EXTERNAL + +' HmGetRef - Lookup ADDRESS reference by key. Returns 0 if not found. +DECLARE SUB LONGINT HmGetRef(Hashmap hm, theKey$) EXTERNAL + +' HmHas - Check if key exists. Returns -1 (true) or 0 (false). +DECLARE SUB SHORTINT HmHas(Hashmap hm, theKey$) EXTERNAL + +' HmType - Get type tag for key. Returns -1 if not found. +DECLARE SUB SHORTINT HmType(Hashmap hm, theKey$) EXTERNAL + +' HmDel - Delete entry by key. Returns HM_SUCCESS or HM_ERR_NOTFOUND. +DECLARE SUB SHORTINT HmDel(Hashmap hm, theKey$) EXTERNAL + +{* ============== Info ============== *} + +' HmCount - Number of entries currently stored +DECLARE SUB LONGINT HmCount(Hashmap hm) EXTERNAL + +' HmCapacity - Current capacity of the hashmap +DECLARE SUB LONGINT HmCapacity(Hashmap hm) EXTERNAL + +{* ============== Iteration ============== *} + +' HmIterReset - Reset iterator to beginning +DECLARE SUB HmIterReset(Hashmap hm) EXTERNAL + +' HmIterNext - Advance to next entry. Returns -1 (true) or 0 (false). +DECLARE SUB SHORTINT HmIterNext(Hashmap hm) EXTERNAL + +' HmIterKey$ - Key at current iterator position +DECLARE SUB STRING HmIterKey$(Hashmap hm) EXTERNAL + +' HmIterVal$ - String value at current position +DECLARE SUB STRING HmIterVal$(Hashmap hm) EXTERNAL + +' HmIterVal& - LONGINT value at current position (also for bool/ref) +DECLARE SUB LONGINT HmIterVal&(Hashmap hm) EXTERNAL + +' HmIterVal! - SINGLE value at current position +DECLARE SUB SINGLE HmIterVal!(Hashmap hm) EXTERNAL + +' HmIterType - Type tag at current position +DECLARE SUB SHORTINT HmIterType(Hashmap hm) EXTERNAL + +{* ============== Higher-Order Iteration ============== *} + +' HmForEach - Call callback for each entry in insertion order. +' Callback: SUB ADDRESS cb(ADDRESS keyPtr, LONGINT rawVal&, +' ADDRESS strPtr, SHORTINT typ%) INVOKABLE +' Use CSTR(keyPtr)/CSTR(strPtr) for string access. Pass as BIND(@MySub). +DECLARE SUB HmForEach(Hashmap hm, ADDRESS fun) EXTERNAL + +{* ============== Builder Pattern ============== *} + +' HmNew - Start building a new hashmap with given capacity +DECLARE SUB HmNew(LONGINT theCap&) EXTERNAL + +' HmAdd$ - Add string entry to builder +DECLARE SUB SHORTINT HmAdd$(theKey$, theVal$) EXTERNAL + +' HmAdd& - Add LONGINT entry to builder +DECLARE SUB SHORTINT HmAdd&(theKey$, LONGINT theVal&) EXTERNAL + +' HmAdd! - Add SINGLE entry to builder +DECLARE SUB SHORTINT HmAdd!(theKey$, SINGLE theVal!) EXTERNAL + +' HmAddRef - Add ADDRESS reference entry to builder +DECLARE SUB SHORTINT HmAddRef(theKey$, ADDRESS theRef&) EXTERNAL + +' HmAddBool - Add boolean entry to builder +DECLARE SUB SHORTINT HmAddBool(theKey$, SHORTINT theVal%) EXTERNAL + +' HmAddNull - Add null entry to builder +DECLARE SUB SHORTINT HmAddNull(theKey$) EXTERNAL + +' HmEnd - Finalize builder, return ADDRESS of completed Hashmap +DECLARE SUB LONGINT HmEnd EXTERNAL + +#endif diff --git a/specs/hashmap-submod-state.txt b/specs/hashmap-submod-state.txt new file mode 100644 index 0000000..3c56121 --- /dev/null +++ b/specs/hashmap-submod-state.txt @@ -0,0 +1,139 @@ +Hashmap Submodule - Implementation State +========================================= + +Spec: specs/hashmap-submod.txt +Branch: hashmap-phase1 + +Phase 1: Core [COMPLETE] + - CLASS Hashmap definition [done] + - HmMake / HmFree / HmClear [done] + - DJB2 hash function [done] + - DIM...ADDRESS overlay pattern [done] + - HmPut$ / HmGet$ / HmHas / HmDel [done] + - HmCount / HmCapacity [done] + - Probe safety (counter to prevent infinite loop) [done] + - Load factor check (after probe, allows update at full load) [done] + - Test: test_core.b (49 tests) [done] + - Emulator verification [done] all 49 pass + +Phase 2: Typed Values [COMPLETE] + - HmPut& / HmPut! / HmPutBool / HmPutNull / HmPutRef [done] + - HmGet& / HmGet! / HmGetRef [done] + - HmType [done] + - Test: test_typed.b (54 tests) [done] + - Emulator verification [done] all 54 pass + +Phase 3: Iteration [COMPLETE] + - _HmOrderAppend internal helper [done] + - Invalidates old order entry on tombstone reuse + - Compacts order array when full + - Appends new backing index + - All 6 HmPut* SUBs call _HmOrderAppend [done] + - HmIterReset / HmIterNext [done] + - HmIterKey$ / HmIterVal$ / HmIterVal& / HmIterVal! / HmIterType [done] + - HmIterNext skips tombstoned and invalidated (-1) entries [done] + - hashmap.h updated with 7 iteration declares [done] + - Test: test_iter.b (13 test groups, 70 asserts) [done] + - Emulator verification [done] all 70 pass + +Phase 3b: HmForEach (Higher-Order Iteration) [COMPLETE] + - HmForEach(Hashmap hm, ADDRESS fun) [done] + - Walks insertion order, calls INVOKE fun(keyPtr, rawVal&, strPtr, typ%) + - Callback uses ADDRESS for strings (proven pattern from list submodule) + - Callback should be INVOKABLE, passed via BIND(@MySub) + - hashmap.h updated with HmForEach declare [done] + - Test: test_foreach.b (7 tests, 18 asserts) [done] + - Empty map, counting, key order, after delete, sum longs, + mixed type counting, string value access via CSTR + - Emulator verification [done] all 13 pass + - Regression: test_iter 70/70 pass [done] + +Phase 3c: Builder Pattern [COMPLETE] + - HmNew / HmEnd [done] + - HmAdd$ / HmAdd& / HmAdd! / HmAddRef / HmAddBool / HmAddNull [done] + - hashmap.h updated with 8 builder declares [done] + - Test: test_builder.b (7 test groups, 30 asserts) [done] + - Basic string, mixed types, nested builder, valid ADDRESS, + error propagation, iteration order, empty builder + - Emulator verification [done] all 30 pass + - Full regression: 216/216 pass (49+54+70+13+30) [done] + +Phase 3d: Internal Deduplication Refactoring [COMPLETE] + - 3 internal helpers extracted: [done] + - _HmFindKey: lookup probe, sets _hmFoundIdx + - _HmPutProbe: insert probe with tombstone tracking, sets _hmSlotIdx/_hmSlotMode + - _HmCommitNew: writes key, status, count++, order append + - 3 module-level result variables: [done] + - _hmFoundIdx, _hmSlotIdx, _hmSlotMode + - 7 lookup consumers refactored to _HmFindKey: [done] + - HmGet$, HmGet&, HmGet!, HmGetRef, HmHas, HmType, HmDel + - 6 Put SUBs refactored to _HmPutProbe + _HmCommitNew: [done] + - HmPut$, HmPut&, HmPut!, HmPutRef, HmPutBool, HmPutNull + - Line count: 984 -> 789 (~20% reduction) [done] + - Public API unchanged [done] + - Emulator verification [done] all 216 pass + - Full regression: 216/216 (49+54+70+13+30) [done] + +Phase 4: JSON Integration (optional) [not started] + - JsonEmitObject / JsonEmitArray + - JsonParse / JsonCountKeys + +Files: + submods/hashmap/hashmap.b - Module source (~790 lines, 38 SUBs) + include/submods/hashmap.h - Header + submods/hashmap/make - Build script + submods/hashmap/test_core.b - Phase 1 tests (49 tests) + submods/hashmap/test_typed.b - Phase 2 tests (54 tests) + submods/hashmap/test_iter.b - Phase 3 tests (70 tests) + submods/hashmap/test_foreach.b - Phase 3b tests (13 tests) + submods/hashmap/test_builder.b - Phase 3c tests (30 tests) + +Design Notes: + - String arrays: HM_KEY_SIZE=64 bytes/key, HM_VAL_SIZE=256 bytes/value + - All 6 backing arrays allocated from Phase 1 (forward compat) + - Type tags set in HmPut$ (HmTypeStr) even before Phase 2 + - Probe counter prevents infinite loop in pathological tombstone cases + - Load factor checked after probe (allows update of existing key at full load) + - DIM...ADDRESS requires CONST for array bound (ACE parser limitation) + -> HM_MAX_BOUND=511 used for all overlays; actual access bounded by hm->cap + - DIM...ADDRESS can't use struct members (hm->status) directly + -> Must copy to local ADDRESS variable first + - Order array uses SHORTINT (-1 as sentinel for invalidated entries) + - _HmOrderAppend compacts order array when orderCount reaches cap + (removes -1 and tombstoned entries, then appends) + - Updates (same key) don't affect order (handled in probe loop before insert) + +Builder Pattern Design Notes: + - Builder uses LONGINT _hmBldPtr at module level (not DECLARE CLASS) + because SHARED with module-level CLASS variables causes exit crashes. + - Each builder SUB does SHARED _hmBldPtr, then local DECLARE CLASS + Hashmap bld, then bld = _hmBldPtr to operate on the ALLOC'd block. + - HmNew ALLOCs a fresh 48-byte CLASS block (_HM_STRUCT_SIZE) for each + builder, so sequential builders produce independent maps. + - HmEnd returns the ALLOC'd ADDRESS. The return assignment MUST be the + last statement in the SUB (ACE clobbers d0 on any subsequent statement). + - Builder maps require TWO cleanup calls: + HmFree(m) — frees the 6 backing arrays (keys, vals, valsL, types, + status, order) that HmMake allocated + FREE map& — frees the 48-byte CLASS struct block that HmNew allocated + For DECLARE CLASS maps (not from builder), only HmFree is needed + because the struct lives in BSS (not ALLOC'd). + - DECLARE CLASS ... ADDRESS syntax does NOT exist for CLASS (only STRUCT). + Instead: DECLARE CLASS Hashmap m, then m = addr& to redirect. + +Internal Helper Design Notes: + - Helpers communicate results via SHARED module-level LONGINTs + (ACE SUBs can't return multiple values or use out-params for LONGINTs) + - _HmFindKey sets _hmFoundIdx to backing index or -1 + - _HmPutProbe sets _hmSlotIdx (slot) and _hmSlotMode (1=update, 0=new, -1=full) + - _HmCommitNew takes idx& as param (not SHARED) for clarity + - Module-level vars declared before first SUB that uses SHARED on them + - Pattern: call helper, check result var, then access type-specific array locally + +ACE Pitfalls Discovered: + - SHARED with module-level DECLARE CLASS causes crashes at program exit + - DECLARE CLASS ... ADDRESS is not valid syntax (use assignment instead) + - Return value assignment (FuncName = expr) must be last statement in SUB; + any subsequent statement clobbers d0 + - ASSERT is a built-in statement that halts on failure (not for test suites) + - "Assert" cannot be used as a SUB name (reserved word) diff --git a/submods/amissl/README.txt b/submods/amissl/README.txt new file mode 100644 index 0000000..ff5965b --- /dev/null +++ b/submods/amissl/README.txt @@ -0,0 +1,4 @@ +AmiSSL Submodule for ACE BASIC +============================== + +Designed by Manfred Bergmann, copyright 2026. diff --git a/submods/hashmap/README.txt b/submods/hashmap/README.txt new file mode 100644 index 0000000..3fc2a5f --- /dev/null +++ b/submods/hashmap/README.txt @@ -0,0 +1,357 @@ +Hashmap Submodule for ACE BASIC +=============================== + +Designed by Manfred Bergmann, copyright 2026. + +A string-keyed hashmap using open addressing with linear probing. +CLASS-based: multiple independent instances, each with own storage. +DJB2 hash, power-of-2 capacity, 70% max load factor, tombstone deletion. + + +Overview +-------- + +A hashmap stores key-value pairs where keys are strings and values can +be any of 6 types: STRING, LONGINT, SINGLE, ADDRESS (ref), boolean, +or null. + +Example: + + #include + + DECLARE CLASS Hashmap map + HmMake(map, HM_MEDIUM) ' 128 slots + + HmPut$(map, "name", "Alice") + HmPut&(map, "age", 30) + HmPut!(map, "score", 9.5) + + PRINT HmGet$(map, "name") ' Alice + PRINT HmGet&(map, "age") ' 30 + PRINT HmHas(map, "name") ' -1 (true) + + HmDel(map, "age") + PRINT HmCount(map) ' 2 + + HmFree(map) + +See test_core.b, test_typed.b, test_iter.b, test_foreach.b, and +test_builder.b for comprehensive examples. + + +Type Suffixes +------------- + +Functions use type suffixes to indicate the data type: + $ - STRING (up to HM_VAL_SIZE-1 = 255 characters) + & - LONGINT (32-bit integer) + ! - SINGLE (32-bit float, Amiga FFP format) + (none for Put/Get) - Ref, Bool, Null have named variants + +Use HmType() to check the type of an entry at runtime: + HmTypeStr = 0 HmTypeLng = 1 HmTypeSng = 2 + HmTypeRef = 3 HmTypeBool = 4 HmTypeNull = 5 + +HmType() returns -1 if the key is not found. + + +Capacity and Sizing +------------------- + +Capacity must be a power of 2. Three presets are provided: + HM_SMALL = 32 (up to ~22 entries at 70% load) + HM_MEDIUM = 128 (up to ~89 entries) + HM_LARGE = 512 (up to ~358 entries) + +The maximum capacity is HM_LARGE (512). This limit comes from the +DIM...ADDRESS overlay bound (HM_MAX_BOUND = 511). + +When the map reaches 70% capacity, Put operations return HM_ERR_FULL. +Updates to existing keys always succeed regardless of load factor. + +Key strings are limited to HM_KEY_SIZE-1 = 63 characters. +String values are limited to HM_VAL_SIZE-1 = 255 characters. + + +Memory Management +----------------- + +There are two ways to create a hashmap, each with different cleanup. + +Method 1: DECLARE CLASS (stack/BSS struct) + + DECLARE CLASS Hashmap map + HmMake(map, HM_MEDIUM) + ' ... use map ... + HmFree(map) ' Frees the 6 backing arrays + + The Hashmap struct itself lives in BSS (static storage). + Only one call to HmFree is needed. + +Method 2: Builder pattern (heap-allocated struct) + + HmNew(HM_MEDIUM) + HmAdd$("name", "Alice") + HmAdd&("age", 30) + LONGINT map& + map& = HmEnd + + ' To use the returned map, create a local CLASS reference: + DECLARE CLASS Hashmap m + m = map& + PRINT HmGet$(m, "name") + + ' Cleanup requires TWO calls: + HmFree(m) ' Frees the 6 backing arrays + FREE map& ' Frees the 48-byte CLASS struct + + The builder ALLOCs a Hashmap struct on the heap (48 bytes). + HmFree cannot free this struct because it does not know whether + the struct was ALLOC'd (builder) or lives in BSS (DECLARE CLASS). + Freeing a BSS address would crash, so HmFree only frees the + backing arrays. The caller must FREE the struct separately. + +Rules: + +1. Always call HmFree before discarding a map. It frees 6 internal + arrays (keys, vals, valsL, types, status, order). + +2. For builder-created maps, also FREE the returned ADDRESS. + Forgetting this leaks 48 bytes per map. + +3. After HmFree, the map is invalid. Do not call any Hm* functions + on it (except HmMake to reinitialize). + +4. HmClear resets all entries but keeps the backing arrays allocated. + Use this to reuse a map without reallocating. + + +Error Handling +-------------- + +Put functions return a SHORTINT status code: + HM_SUCCESS = 0 Operation succeeded + HM_ERR_FULL = -1 Map is at 70% load, cannot insert new key + HM_ERR_NOTFOUND = -2 Key not found (returned by HmDel) + +Get functions return a default value when the key is not found: + HmGet$ returns "" + HmGet& returns 0 + HmGet! returns 0 + HmGetRef returns 0 + +Use HmHas to distinguish "key not found" from "key exists with +default value" (e.g., a LONGINT entry that is legitimately 0). + + +Iteration +--------- + +Entries are iterated in insertion order. Deleted entries are skipped. +Updating an existing key does not change its position in the order. + +Cursor-based iteration: + + HmIterReset(map) + WHILE HmIterNext(map) + PRINT HmIterKey$(map); " = "; + IF HmIterType(map) = HmTypeStr THEN + PRINT HmIterVal$(map) + ELSEIF HmIterType(map) = HmTypeLng THEN + PRINT HmIterVal&(map) + END IF + WEND + +The iterator state is stored in the Hashmap instance (cursor, curIdx). +Call HmIterReset to restart iteration from the beginning. + +Higher-order iteration with HmForEach: + + SUB ADDRESS PrintEntry(ADDRESS kPtr, LONGINT rawVal&, ~ + ADDRESS sPtr, SHORTINT typ%) INVOKABLE + PRINT CSTR(kPtr); " (type "; typ%; ")" + END SUB + + HmForEach(map, BIND(@PrintEntry)) + +Callback signature: + SUB ADDRESS cb(ADDRESS keyPtr, LONGINT rawVal&, + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + + keyPtr - Pointer to key string (use CSTR(keyPtr)) + rawVal& - Raw LONGINT value (for Lng, Ref, Bool types) + strPtr - Pointer to string value (use CSTR(strPtr)) + typ% - Type tag (HmTypeStr, HmTypeLng, etc.) + +The callback must be declared INVOKABLE and passed via BIND(@MySub). +See test_foreach.b for complete examples. + + +Builder Pattern +--------------- + +The builder provides a fluent API for constructing hashmaps: + + HmNew(HM_SMALL) + HmAdd$("greeting", "hello") + HmAdd&("count", 42) + HmAddBool("active", -1) + HmAddNull("empty") + LONGINT map& + map& = HmEnd + +Only one builder can be active at a time. Nested builders are +supported if inner builders complete before outer ones resume +(the inner HmEnd returns before the outer HmAdd continues). + +See test_builder.b for nested builder and error propagation examples. + + +Nested Hashmaps +--------------- + +Use HmPutRef / HmGetRef to store references to other hashmaps: + + DECLARE CLASS Hashmap outer, inner + HmMake(outer, HM_SMALL) + HmMake(inner, HM_SMALL) + + HmPut$(inner, "city", "Berlin") + HmPutRef(outer, "address", inner) + + ' Retrieve the nested map + DECLARE CLASS Hashmap got + got = HmGetRef(outer, "address") + PRINT HmGet$(got, "city") ' Berlin + + ' Free inner map first, then outer + HmFree(inner) + HmFree(outer) + +HmPutRef stores the ADDRESS of the inner map's CLASS instance. +The hashmap does not manage the lifetime of referenced objects. +The caller is responsible for freeing nested maps. + +See test_typed.b for a complete nested hashmap example. + + +Building the Module +------------------- + +On Amiga (or emulator): + + cd ACE:submods/hashmap + bas -m hashmap + +This compiles hashmap.b as a module, producing hashmap.o + + +Running Tests +------------- + +On Amiga (or emulator): + + cd ACE:submods/hashmap + bas test_core hashmap.o + test_core + + bas test_typed hashmap.o + test_typed + + bas test_iter hashmap.o + test_iter + + bas test_foreach hashmap.o + test_foreach + + bas test_builder hashmap.o + test_builder + +5 test suites, 216 total assertions. + + +Using in Your Programs +---------------------- + +1. Include the header: + + #include + +2. Compile your program, linking the module: + + bas myprogram ace:submods/hashmap/hashmap.o + + Or use REM #using at the top of your .b file: + + REM #using hashmap.o + + (requires hashmap.o to be in the same directory or on the path) + + +Files +----- + +hashmap.b - Library source code (~790 lines, 38 SUBs) +hashmap.o - Compiled module (after building) +test_core.b - Phase 1 tests: core operations (49 assertions) +test_typed.b - Phase 2 tests: typed values (54 assertions) +test_iter.b - Phase 3 tests: iteration (70 assertions) +test_foreach.b - Phase 3b tests: HmForEach (13 assertions) +test_builder.b - Phase 3c tests: builder pattern (30 assertions) +README.txt - This file + +include/submods/hashmap.h - Header file with declarations + + +Function Reference +------------------ + +See include/submods/hashmap.h for declarations and inline documentation. + +Factory & Cleanup: + HmMake(hm, cap&) Initialize with power-of-2 capacity + HmFree(hm) Free all backing arrays + HmClear(hm) Reset entries, keep arrays allocated + +Put (insert or update): + HmPut$(hm, key$, val$) String value -> HM_SUCCESS or HM_ERR_FULL + HmPut&(hm, key$, val&) LONGINT value -> HM_SUCCESS or HM_ERR_FULL + HmPut!(hm, key$, val!) SINGLE value -> HM_SUCCESS or HM_ERR_FULL + HmPutRef(hm, key$, ref&) ADDRESS value -> HM_SUCCESS or HM_ERR_FULL + HmPutBool(hm, key$, val%) Boolean (0 or 1) -> HM_SUCCESS or HM_ERR_FULL + HmPutNull(hm, key$) Null (type only) -> HM_SUCCESS or HM_ERR_FULL + +Get (lookup): + HmGet$(hm, key$) -> STRING ("" if not found) + HmGet&(hm, key$) -> LONGINT (0 if not found) + HmGet!(hm, key$) -> SINGLE (0 if not found) + HmGetRef(hm, key$) -> LONGINT/ADDRESS (0 if not found) + +Query: + HmHas(hm, key$) -> -1 (true) or 0 (false) + HmType(hm, key$) -> type tag (0-5) or -1 if not found + HmDel(hm, key$) -> HM_SUCCESS or HM_ERR_NOTFOUND + HmCount(hm) -> number of entries + HmCapacity(hm) -> current capacity + +Iteration (insertion order): + HmIterReset(hm) Reset iterator to beginning + HmIterNext(hm) Advance; -1 (has entry) or 0 (done) + HmIterKey$(hm) Key at current position + HmIterVal$(hm) String value at current position + HmIterVal&(hm) LONGINT value at current position + HmIterVal!(hm) SINGLE value at current position + HmIterType(hm) Type tag at current position + +Higher-order: + HmForEach(hm, fun) Call fun for each entry (BIND + INVOKABLE) + +Builder: + HmNew(cap&) Start builder with given capacity + HmAdd$(key$, val$) Add string entry + HmAdd&(key$, val&) Add LONGINT entry + HmAdd!(key$, val!) Add SINGLE entry + HmAddRef(key$, ref&) Add ADDRESS entry + HmAddBool(key$, val%) Add boolean entry + HmAddNull(key$) Add null entry + HmEnd -> LONGINT (ADDRESS of completed Hashmap) diff --git a/submods/hashmap/hashmap.b b/submods/hashmap/hashmap.b new file mode 100644 index 0000000..f19e551 --- /dev/null +++ b/submods/hashmap/hashmap.b @@ -0,0 +1,788 @@ +{* +** Hashmap - String-keyed hashmap with open addressing and linear probing +** +** CLASS-based: multiple independent instances, each with own storage. +** ALLOC'd backing arrays with DIM...ADDRESS overlay for array access. +** DJB2 hash, power-of-2 capacity, 70% max load factor, tombstone deletion. +** +** Usage: +** #include +** DECLARE CLASS Hashmap map +** HmMake(map, HM_MEDIUM) +** HmPut$(map, "name", "Alice") +** PRINT HmGet$(map, "name") +** HmFree(map) +*} + +{* ============== Constants ============== *} + +' Slot status +CONST HM_EMPTY = 0 +CONST HM_OCCUPIED = 1 +CONST HM_TOMBSTONE = 2 + +' Error codes +CONST HM_SUCCESS = 0 +CONST HM_ERR_FULL = -1 +CONST HM_ERR_NOTFOUND = -2 +CONST HM_ERR_CAPACITY = -3 + +' Value type tags +CONST HmTypeStr = 0 +CONST HmTypeLng = 1 +CONST HmTypeSng = 2 +CONST HmTypeRef = 3 +CONST HmTypeBool = 4 +CONST HmTypeNull = 5 + +' Capacity presets +CONST HM_SMALL = 32 +CONST HM_MEDIUM = 128 +CONST HM_LARGE = 512 + +' String buffer sizes per element +CONST HM_KEY_SIZE = 64 +CONST HM_VAL_SIZE = 256 + +' Max bound for DIM...ADDRESS overlays (HM_LARGE - 1) +' DIM requires a CONST bound. Actual access is limited by hm->cap. +CONST HM_MAX_BOUND = 511 + +{* ============== CLASS Definition ============== *} + +CLASS Hashmap + ADDRESS keys + ADDRESS vals + ADDRESS valsL + ADDRESS types + ADDRESS status + ADDRESS order + LONGINT cap + LONGINT count + LONGINT orderCount + LONGINT cursor + LONGINT curIdx +END CLASS + +{* ============== Module-Level State ============== *} + +' Size of Hashmap CLASS: 4 (descriptor) + 6*4 (ADDRESS) + 5*4 (LONGINT) = 48 +CONST _HM_STRUCT_SIZE = 48 + +' Internal helper result variables +LONGINT _hmFoundIdx ' _HmFindKey result: backing index or -1 +LONGINT _hmSlotIdx ' _HmPutProbe result: slot index +LONGINT _hmSlotMode ' _HmPutProbe result: 1=update, 0=new, -1=full + +{* ============== Internal: Hash Function (DJB2) ============== *} + +SUB LONGINT _HmHash(theKey$, LONGINT theCap&) + LONGINT h& + SHORTINT i% + + h& = 5381 + FOR i% = 1 TO LEN(theKey$) + h& = h& * 33 + ASC(MID$(theKey$, i%, 1)) + NEXT + _HmHash = h& AND (theCap& - 1) +END SUB + +{* ============== Internal: Order Array Maintenance ============== *} + +{* +** Append a backing-array index to the insertion-order array. +** If the slot was previously used (tombstone reuse), the old +** order entry is invalidated first. Compacts if order array is full. +*} +SUB _HmOrderAppend(Hashmap hm, LONGINT backIdx&) + ADDRESS oAddr, stAddr + LONGINT i&, j& + + oAddr = hm->order + stAddr = hm->status + DIM ord%(HM_MAX_BOUND) ADDRESS oAddr + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + + ' Invalidate any old order entry for this slot (tombstone reuse) + i& = 0 + WHILE i& < hm->orderCount + IF ord%(i&) = backIdx& THEN + ord%(i&) = -1 + i& = hm->orderCount + ELSE + i& = i& + 1 + END IF + WEND + + ' Compact if order array is full + IF hm->orderCount >= hm->cap THEN + j& = 0 + FOR i& = 0 TO hm->orderCount - 1 + IF ord%(i&) >= 0 AND st%(ord%(i&)) = HM_OCCUPIED THEN + ord%(j&) = ord%(i&) + j& = j& + 1 + END IF + NEXT + hm->orderCount = j& + END IF + + ' Append new entry + ord%(hm->orderCount) = backIdx& + hm->orderCount = hm->orderCount + 1 +END SUB + +{* ============== Internal: Lookup Probe ============== *} + +{* +** Find an existing key's backing index via linear probe. +** Sets _hmFoundIdx to the index, or -1 if not found. +*} +SUB _HmFindKey(Hashmap hm, theKey$) + SHARED _hmFoundIdx + LONGINT idx&, probes&, capVal& + ADDRESS kAddr, stAddr + + capVal& = hm->cap + kAddr = hm->keys + stAddr = hm->status + + DIM k$(HM_MAX_BOUND) SIZE HM_KEY_SIZE ADDRESS kAddr + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + + idx& = _HmHash(theKey$, capVal&) + probes& = 0 + + WHILE probes& < capVal& AND st%(idx&) <> HM_EMPTY + IF st%(idx&) = HM_OCCUPIED AND k$(idx&) = theKey$ THEN + _hmFoundIdx = idx& + EXIT SUB + END IF + idx& = (idx& + 1) AND (capVal& - 1) + probes& = probes& + 1 + WEND + + _hmFoundIdx = -1 +END SUB + +{* ============== Internal: Insert Probe ============== *} + +{* +** Find the slot for a put operation. Handles tombstone tracking +** and load-factor check. +** Sets _hmSlotIdx and _hmSlotMode: +** _hmSlotMode = 1 -> key exists at _hmSlotIdx (update) +** _hmSlotMode = 0 -> new slot at _hmSlotIdx (insert) +** _hmSlotMode = -1 -> map full (no insert possible) +*} +SUB _HmPutProbe(Hashmap hm, theKey$) + SHARED _hmSlotIdx, _hmSlotMode + LONGINT idx&, tombIdx&, maxLoad&, probes&, capVal& + ADDRESS kAddr, stAddr + + capVal& = hm->cap + kAddr = hm->keys + stAddr = hm->status + + DIM k$(HM_MAX_BOUND) SIZE HM_KEY_SIZE ADDRESS kAddr + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + + idx& = _HmHash(theKey$, capVal&) + tombIdx& = -1 + probes& = 0 + + WHILE probes& < capVal& AND st%(idx&) <> HM_EMPTY + IF st%(idx&) = HM_OCCUPIED THEN + IF k$(idx&) = theKey$ THEN + _hmSlotIdx = idx& + _hmSlotMode = 1 + EXIT SUB + END IF + ELSEIF tombIdx& = -1 THEN + tombIdx& = idx& + END IF + idx& = (idx& + 1) AND (capVal& - 1) + probes& = probes& + 1 + WEND + + ' Key not found - check load factor before inserting + maxLoad& = (capVal& * 7) \ 10 + IF hm->count >= maxLoad& THEN + _hmSlotMode = -1 + EXIT SUB + END IF + + ' Use tombstone slot if available, else empty slot + IF tombIdx& >= 0 THEN + _hmSlotIdx = tombIdx& + _hmSlotMode = 0 + ELSEIF probes& >= capVal& THEN + _hmSlotMode = -1 + ELSE + _hmSlotIdx = idx& + _hmSlotMode = 0 + END IF +END SUB + +{* ============== Internal: Commit New Entry ============== *} + +{* +** Write key, set status to OCCUPIED, increment count, append to order. +** Called by Put* SUBs only when _hmSlotMode = 0 (new entry). +*} +SUB _HmCommitNew(Hashmap hm, theKey$, LONGINT idx&) + ADDRESS kAddr, stAddr + + kAddr = hm->keys + stAddr = hm->status + + DIM k$(HM_MAX_BOUND) SIZE HM_KEY_SIZE ADDRESS kAddr + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + + k$(idx&) = theKey$ + st%(idx&) = HM_OCCUPIED + hm->count = hm->count + 1 + _HmOrderAppend(hm, idx&) +END SUB + +{* ============== Factory & Cleanup ============== *} + +SUB HmMake(Hashmap hm, LONGINT theCap&) EXTERNAL + SHORTINT i + ADDRESS stAddr + + ' Validate power of 2 + IF theCap& <= 0 OR (theCap& AND (theCap& - 1)) <> 0 THEN + PRINT "HmMake: capacity must be power of 2" + EXIT SUB + END IF + + hm->cap = theCap& + hm->count = 0 + hm->orderCount = 0 + hm->cursor = -1 + hm->curIdx = -1 + + ' Allocate backing arrays + hm->keys = ALLOC(theCap& * HM_KEY_SIZE) + hm->vals = ALLOC(theCap& * HM_VAL_SIZE) + hm->valsL = ALLOC(theCap& * 4) + hm->types = ALLOC(theCap& * 2) + hm->status = ALLOC(theCap& * 2) + hm->order = ALLOC(theCap& * 2) + + ' Clear status array (all slots empty) + stAddr = hm->status + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + FOR i = 0 TO theCap& - 1 + st%(i) = HM_EMPTY + NEXT +END SUB + +SUB HmFree(Hashmap hm) EXTERNAL + IF hm->keys <> 0 THEN FREE hm->keys + IF hm->vals <> 0 THEN FREE hm->vals + IF hm->valsL <> 0 THEN FREE hm->valsL + IF hm->types <> 0 THEN FREE hm->types + IF hm->status <> 0 THEN FREE hm->status + IF hm->order <> 0 THEN FREE hm->order + hm->keys = 0 + hm->vals = 0 + hm->valsL = 0 + hm->types = 0 + hm->status = 0 + hm->order = 0 + hm->cap = 0 + hm->count = 0 + hm->orderCount = 0 +END SUB + +SUB HmClear(Hashmap hm) EXTERNAL + SHORTINT i + ADDRESS stAddr + + stAddr = hm->status + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + FOR i = 0 TO hm->cap - 1 + st%(i) = HM_EMPTY + NEXT + hm->count = 0 + hm->orderCount = 0 + hm->cursor = -1 + hm->curIdx = -1 +END SUB + +{* ============== Core: Put String ============== *} + +SUB SHORTINT HmPut$(Hashmap hm, theKey$, theVal$) EXTERNAL + SHARED _hmSlotIdx, _hmSlotMode + ADDRESS vAddr, tpAddr + + _HmPutProbe(hm, theKey$) + IF _hmSlotMode < 0 THEN + HmPut$ = HM_ERR_FULL + EXIT SUB + END IF + + vAddr = hm->vals + tpAddr = hm->types + DIM v$(HM_MAX_BOUND) SIZE HM_VAL_SIZE ADDRESS vAddr + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + v$(_hmSlotIdx) = theVal$ + tp%(_hmSlotIdx) = HmTypeStr + IF _hmSlotMode = 0 THEN _HmCommitNew(hm, theKey$, _hmSlotIdx) + HmPut$ = HM_SUCCESS +END SUB + +{* ============== Core: Put Long ============== *} + +SUB SHORTINT HmPut&(Hashmap hm, theKey$, LONGINT theVal&) EXTERNAL + SHARED _hmSlotIdx, _hmSlotMode + ADDRESS vlAddr, tpAddr + + _HmPutProbe(hm, theKey$) + IF _hmSlotMode < 0 THEN + HmPut& = HM_ERR_FULL + EXIT SUB + END IF + + vlAddr = hm->valsL + tpAddr = hm->types + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + vL&(_hmSlotIdx) = theVal& + tp%(_hmSlotIdx) = HmTypeLng + IF _hmSlotMode = 0 THEN _HmCommitNew(hm, theKey$, _hmSlotIdx) + HmPut& = HM_SUCCESS +END SUB + +{* ============== Core: Put Single ============== *} + +SUB SHORTINT HmPut!(Hashmap hm, theKey$, SINGLE theVal!) EXTERNAL + SHARED _hmSlotIdx, _hmSlotMode + ADDRESS vlAddr, tpAddr + + _HmPutProbe(hm, theKey$) + IF _hmSlotMode < 0 THEN + HmPut! = HM_ERR_FULL + EXIT SUB + END IF + + vlAddr = hm->valsL + tpAddr = hm->types + DIM vF!(HM_MAX_BOUND) ADDRESS vlAddr + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + vF!(_hmSlotIdx) = theVal! + tp%(_hmSlotIdx) = HmTypeSng + IF _hmSlotMode = 0 THEN _HmCommitNew(hm, theKey$, _hmSlotIdx) + HmPut! = HM_SUCCESS +END SUB + +{* ============== Core: Put Ref ============== *} + +SUB SHORTINT HmPutRef(Hashmap hm, theKey$, ADDRESS theRef&) EXTERNAL + SHARED _hmSlotIdx, _hmSlotMode + ADDRESS vlAddr, tpAddr + + _HmPutProbe(hm, theKey$) + IF _hmSlotMode < 0 THEN + HmPutRef = HM_ERR_FULL + EXIT SUB + END IF + + vlAddr = hm->valsL + tpAddr = hm->types + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + vL&(_hmSlotIdx) = theRef& + tp%(_hmSlotIdx) = HmTypeRef + IF _hmSlotMode = 0 THEN _HmCommitNew(hm, theKey$, _hmSlotIdx) + HmPutRef = HM_SUCCESS +END SUB + +{* ============== Core: Put Bool ============== *} + +SUB SHORTINT HmPutBool(Hashmap hm, theKey$, SHORTINT theVal%) EXTERNAL + SHARED _hmSlotIdx, _hmSlotMode + ADDRESS vlAddr, tpAddr + + _HmPutProbe(hm, theKey$) + IF _hmSlotMode < 0 THEN + HmPutBool = HM_ERR_FULL + EXIT SUB + END IF + + vlAddr = hm->valsL + tpAddr = hm->types + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + IF theVal% THEN vL&(_hmSlotIdx) = 1 ELSE vL&(_hmSlotIdx) = 0 + tp%(_hmSlotIdx) = HmTypeBool + IF _hmSlotMode = 0 THEN _HmCommitNew(hm, theKey$, _hmSlotIdx) + HmPutBool = HM_SUCCESS +END SUB + +{* ============== Core: Put Null ============== *} + +SUB SHORTINT HmPutNull(Hashmap hm, theKey$) EXTERNAL + SHARED _hmSlotIdx, _hmSlotMode + ADDRESS tpAddr + + _HmPutProbe(hm, theKey$) + IF _hmSlotMode < 0 THEN + HmPutNull = HM_ERR_FULL + EXIT SUB + END IF + + tpAddr = hm->types + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + tp%(_hmSlotIdx) = HmTypeNull + IF _hmSlotMode = 0 THEN _HmCommitNew(hm, theKey$, _hmSlotIdx) + HmPutNull = HM_SUCCESS +END SUB + +{* ============== Core: Get String ============== *} + +SUB STRING HmGet$(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + ADDRESS vAddr + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx < 0 THEN + HmGet$ = "" + EXIT SUB + END IF + + vAddr = hm->vals + DIM v$(HM_MAX_BOUND) SIZE HM_VAL_SIZE ADDRESS vAddr + HmGet$ = v$(_hmFoundIdx) +END SUB + +{* ============== Core: Get Long ============== *} + +SUB LONGINT HmGet&(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + ADDRESS vlAddr + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx < 0 THEN + HmGet& = 0 + EXIT SUB + END IF + + vlAddr = hm->valsL + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + HmGet& = vL&(_hmFoundIdx) +END SUB + +{* ============== Core: Get Single ============== *} + +SUB SINGLE HmGet!(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + ADDRESS vlAddr + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx < 0 THEN + HmGet! = 0 + EXIT SUB + END IF + + vlAddr = hm->valsL + DIM vF!(HM_MAX_BOUND) ADDRESS vlAddr + HmGet! = vF!(_hmFoundIdx) +END SUB + +{* ============== Core: Get Ref ============== *} + +SUB LONGINT HmGetRef(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + ADDRESS vlAddr + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx < 0 THEN + HmGetRef = 0 + EXIT SUB + END IF + + vlAddr = hm->valsL + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + HmGetRef = vL&(_hmFoundIdx) +END SUB + +{* ============== Core: Has Key ============== *} + +SUB SHORTINT HmHas(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx >= 0 THEN HmHas = -1 ELSE HmHas = 0 +END SUB + +{* ============== Core: Type ============== *} + +SUB SHORTINT HmType(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + ADDRESS tpAddr + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx < 0 THEN + HmType = -1 + EXIT SUB + END IF + + tpAddr = hm->types + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + HmType = tp%(_hmFoundIdx) +END SUB + +{* ============== Core: Delete ============== *} + +SUB SHORTINT HmDel(Hashmap hm, theKey$) EXTERNAL + SHARED _hmFoundIdx + ADDRESS stAddr + + _HmFindKey(hm, theKey$) + IF _hmFoundIdx < 0 THEN + HmDel = HM_ERR_NOTFOUND + EXIT SUB + END IF + + stAddr = hm->status + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + st%(_hmFoundIdx) = HM_TOMBSTONE + hm->count = hm->count - 1 + HmDel = HM_SUCCESS +END SUB + +{* ============== Info ============== *} + +SUB LONGINT HmCount(Hashmap hm) EXTERNAL + HmCount = hm->count +END SUB + +SUB LONGINT HmCapacity(Hashmap hm) EXTERNAL + HmCapacity = hm->cap +END SUB + +{* ============== Iteration ============== *} + +{* +** Iterator walks entries in insertion order. +** Cursor state is stored in the Hashmap instance itself. +** Skips tombstoned entries and invalidated order slots. +*} + +SUB HmIterReset(Hashmap hm) EXTERNAL + hm->cursor = -1 + hm->curIdx = -1 +END SUB + +SUB SHORTINT HmIterNext(Hashmap hm) EXTERNAL + ADDRESS oAddr, stAddr + LONGINT pos& + SHORTINT backIdx% + + oAddr = hm->order + stAddr = hm->status + DIM ord%(HM_MAX_BOUND) ADDRESS oAddr + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + + pos& = hm->cursor + 1 + WHILE pos& < hm->orderCount + backIdx% = ord%(pos&) + IF backIdx% >= 0 AND st%(backIdx%) = HM_OCCUPIED THEN + hm->cursor = pos& + hm->curIdx = backIdx% + HmIterNext = -1 + EXIT SUB + END IF + pos& = pos& + 1 + WEND + + ' No more entries + hm->cursor = hm->orderCount + hm->curIdx = -1 + HmIterNext = 0 +END SUB + +SUB STRING HmIterKey$(Hashmap hm) EXTERNAL + ADDRESS kAddr + + IF hm->curIdx < 0 THEN + HmIterKey$ = "" + EXIT SUB + END IF + + kAddr = hm->keys + DIM k$(HM_MAX_BOUND) SIZE HM_KEY_SIZE ADDRESS kAddr + HmIterKey$ = k$(hm->curIdx) +END SUB + +SUB STRING HmIterVal$(Hashmap hm) EXTERNAL + ADDRESS vAddr + + IF hm->curIdx < 0 THEN + HmIterVal$ = "" + EXIT SUB + END IF + + vAddr = hm->vals + DIM v$(HM_MAX_BOUND) SIZE HM_VAL_SIZE ADDRESS vAddr + HmIterVal$ = v$(hm->curIdx) +END SUB + +SUB LONGINT HmIterVal&(Hashmap hm) EXTERNAL + ADDRESS vlAddr + + IF hm->curIdx < 0 THEN + HmIterVal& = 0 + EXIT SUB + END IF + + vlAddr = hm->valsL + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + HmIterVal& = vL&(hm->curIdx) +END SUB + +SUB SINGLE HmIterVal!(Hashmap hm) EXTERNAL + ADDRESS vlAddr + + IF hm->curIdx < 0 THEN + HmIterVal! = 0 + EXIT SUB + END IF + + vlAddr = hm->valsL + DIM vF!(HM_MAX_BOUND) ADDRESS vlAddr + HmIterVal! = vF!(hm->curIdx) +END SUB + +SUB SHORTINT HmIterType(Hashmap hm) EXTERNAL + ADDRESS tpAddr + + IF hm->curIdx < 0 THEN + HmIterType = -1 + EXIT SUB + END IF + + tpAddr = hm->types + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + HmIterType = tp%(hm->curIdx) +END SUB + +{* ============== Higher-Order Iteration ============== *} + +{* +** HmForEach - Call a function for each entry in insertion order. +** Callback signature: +** SUB ADDRESS cb(ADDRESS keyPtr, LONGINT rawVal&, +** ADDRESS strPtr, SHORTINT typ%) INVOKABLE +** Use CSTR(keyPtr) and CSTR(strPtr) in callback for string access. +** Pass callback as BIND(@MySub). +*} +SUB HmForEach(Hashmap hm, ADDRESS fun) EXTERNAL + ADDRESS oAddr, stAddr, vlAddr, tpAddr, kPtr, vPtr + LONGINT pos&, bIdx& + + oAddr = hm->order + stAddr = hm->status + vlAddr = hm->valsL + tpAddr = hm->types + + DIM ord%(HM_MAX_BOUND) ADDRESS oAddr + DIM st%(HM_MAX_BOUND) ADDRESS stAddr + DIM vL&(HM_MAX_BOUND) ADDRESS vlAddr + DIM tp%(HM_MAX_BOUND) ADDRESS tpAddr + + pos& = 0 + WHILE pos& < hm->orderCount + bIdx& = ord%(pos&) + IF bIdx& >= 0 AND st%(bIdx&) = HM_OCCUPIED THEN + kPtr = hm->keys + bIdx& * HM_KEY_SIZE + vPtr = hm->vals + bIdx& * HM_VAL_SIZE + INVOKE fun(kPtr, vL&(bIdx&), vPtr, tp%(bIdx&)) + END IF + pos& = pos& + 1 + WEND +END SUB + +{* ============== Builder Pattern ============== *} + +{* +** Fluent construction: HmNew -> HmAdd* -> HmEnd +** Uses a LONGINT to hold the builder's Hashmap ADDRESS. +** Each SUB creates a local DECLARE CLASS to operate on it. +** One builder at a time; inner maps must finish before outer. +*} + +LONGINT _hmBldPtr + +SUB HmNew(LONGINT theCap&) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + + ' ALLOC a fresh CLASS block so each builder is independent + _hmBldPtr = ALLOC(_HM_STRUCT_SIZE) + bld = _hmBldPtr + HmMake(bld, theCap&) +END SUB + +SUB SHORTINT HmAdd$(theKey$, theVal$) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + bld = _hmBldPtr + HmAdd$ = HmPut$(bld, theKey$, theVal$) +END SUB + +SUB SHORTINT HmAdd&(theKey$, LONGINT theVal&) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + bld = _hmBldPtr + HmAdd& = HmPut&(bld, theKey$, theVal&) +END SUB + +SUB SHORTINT HmAdd!(theKey$, SINGLE theVal!) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + bld = _hmBldPtr + HmAdd! = HmPut!(bld, theKey$, theVal!) +END SUB + +SUB SHORTINT HmAddRef(theKey$, ADDRESS theRef&) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + bld = _hmBldPtr + HmAddRef = HmPutRef(bld, theKey$, theRef&) +END SUB + +SUB SHORTINT HmAddBool(theKey$, SHORTINT theVal%) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + bld = _hmBldPtr + HmAddBool = HmPutBool(bld, theKey$, theVal%) +END SUB + +SUB SHORTINT HmAddNull(theKey$) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + bld = _hmBldPtr + HmAddNull = HmPutNull(bld, theKey$) +END SUB + +{* +** HmEnd - Return the builder's Hashmap ADDRESS. +** The BSS-backed CLASS instance from HmNew is returned directly. +** Clear the builder pointer for next use. +*} +SUB LONGINT HmEnd EXTERNAL + SHARED _hmBldPtr + LONGINT retVal& + retVal& = _hmBldPtr + _hmBldPtr = 0 + HmEnd = retVal& +END SUB diff --git a/submods/hashmap/make b/submods/hashmap/make new file mode 100644 index 0000000..ddb28aa --- /dev/null +++ b/submods/hashmap/make @@ -0,0 +1,2 @@ +; Build hashmap module +execute ACE:bin/bas -m hashmap diff --git a/submods/hashmap/test_core.b b/submods/hashmap/test_core.b new file mode 100644 index 0000000..9735c05 --- /dev/null +++ b/submods/hashmap/test_core.b @@ -0,0 +1,239 @@ +REM #using ace:submods/hashmap/hashmap.o + +{* +** Hashmap Phase 1: Core Tests +** Tests put/get/has/del, collisions, tombstones, +** multiple instances, load factor, clear. +*} + +#include + +SHORTINT _passed, _failed + +{* ============== Assertion Helpers ============== *} + +SUB AssertTrue(SHORTINT condition, msg$) + SHARED _passed, _failed + IF condition THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$ + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEqStr(actual$, expected$, msg$) + SHARED _passed, _failed + IF actual$ = expected$ THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected '"; expected$; "' got '"; actual$; "')" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq&(LONGINT actual, LONGINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq%(SHORTINT actual, SHORTINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +{* ============== Test Suite ============== *} + +PRINT "=== Hashmap Phase 1: Core Tests ===" +PRINT + +_passed = 0 +_failed = 0 + +SHORTINT rc% + +{* --- Test 1: Make and Capacity --- *} +PRINT "--- Make / Capacity ---" +DECLARE CLASS Hashmap m +HmMake(m, HM_SMALL) +AssertEq&(HmCapacity(m), 32, "capacity should be 32") +AssertEq&(HmCount(m), 0, "initial count should be 0") + +{* --- Test 2: Put and Get strings --- *} +PRINT "--- Put / Get ---" +rc% = HmPut$(m, "name", "Alice") +AssertEq%(rc%, 0, "put name should succeed") +AssertEqStr(HmGet$(m, "name"), "Alice", "get name") + +rc% = HmPut$(m, "city", "Berlin") +AssertEq%(rc%, 0, "put city should succeed") +AssertEqStr(HmGet$(m, "city"), "Berlin", "get city") + +AssertEq&(HmCount(m), 2, "count after 2 puts") + +{* --- Test 3: Get nonexistent key --- *} +PRINT "--- Get missing ---" +AssertEqStr(HmGet$(m, "missing"), "", "get missing returns empty") + +{* --- Test 4: Update existing key --- *} +PRINT "--- Update ---" +rc% = HmPut$(m, "name", "Bob") +AssertEq%(rc%, 0, "update name should succeed") +AssertEqStr(HmGet$(m, "name"), "Bob", "get updated name") +AssertEq&(HmCount(m), 2, "count unchanged after update") + +{* --- Test 5: Has --- *} +PRINT "--- Has ---" +AssertTrue(HmHas(m, "name"), "has name") +AssertTrue(HmHas(m, "city"), "has city") +AssertTrue(NOT HmHas(m, "missing"), "not has missing") + +{* --- Test 6: Delete --- *} +PRINT "--- Delete ---" +rc% = HmDel(m, "city") +AssertEq%(rc%, 0, "del city should succeed") +AssertTrue(NOT HmHas(m, "city"), "city gone after del") +AssertEqStr(HmGet$(m, "city"), "", "get deleted city returns empty") +AssertEq&(HmCount(m), 1, "count after delete") + +rc% = HmDel(m, "nonexistent") +AssertEq%(rc%, -2, "del nonexistent returns NOT_FOUND") + +{* --- Test 7: Reinsert after delete (tombstone reuse) --- *} +PRINT "--- Tombstone reuse ---" +rc% = HmPut$(m, "city", "Paris") +AssertEq%(rc%, 0, "reinsert city should succeed") +AssertEqStr(HmGet$(m, "city"), "Paris", "get reinserted city") +AssertEq&(HmCount(m), 2, "count after reinsert") + +{* --- Test 8: Many keys (collision handling) --- *} +PRINT "--- Collisions ---" +HmClear(m) +SHORTINT i%, allOk% +STRING kk$ + +FOR i% = 0 TO 19 + kk$ = "key" + LTRIM$(STR$(i%)) + rc% = HmPut$(m, kk$, "val" + LTRIM$(STR$(i%))) + IF rc% <> 0 THEN + PRINT "FAIL: put "; kk$; " returned"; rc% + _failed = _failed + 1 + END IF +NEXT +AssertEq&(HmCount(m), 20, "count after 20 inserts") + +' Verify all 20 keys +allOk% = -1 +FOR i% = 0 TO 19 + kk$ = "key" + LTRIM$(STR$(i%)) + IF HmGet$(m, kk$) <> "val" + LTRIM$(STR$(i%)) THEN + PRINT "FAIL: get "; kk$; " wrong value" + allOk% = 0 + END IF +NEXT +IF allOk% THEN + _passed = _passed + 1 +ELSE + _failed = _failed + 1 +END IF + +{* --- Test 9: Delete then lookup across probe chain --- *} +PRINT "--- Delete in probe chain ---" +' Delete a middle key, verify others still found +rc% = HmDel(m, "key5") +AssertEq%(rc%, 0, "del key5 should succeed") +AssertTrue(NOT HmHas(m, "key5"), "key5 gone") +AssertTrue(HmHas(m, "key4"), "key4 still found") +AssertTrue(HmHas(m, "key6"), "key6 still found") +AssertTrue(HmHas(m, "key0"), "key0 still found") +AssertTrue(HmHas(m, "key19"), "key19 still found") + +{* --- Test 10: Load factor limit --- *} +PRINT "--- Load factor ---" +HmFree(m) + +DECLARE CLASS Hashmap sm +HmMake(sm, HM_SMALL) +FOR i% = 0 TO 21 + kk$ = "k" + LTRIM$(STR$(i%)) + rc% = HmPut$(sm, kk$, "v") +NEXT +AssertEq&(HmCount(sm), 22, "22 entries at max load") + +rc% = HmPut$(sm, "overflow", "v") +AssertEq%(rc%, -1, "23rd insert should return ERR_FULL") +AssertEq&(HmCount(sm), 22, "count still 22 after failed insert") + +' Update existing at full load should still work +rc% = HmPut$(sm, "k0", "updated") +AssertEq%(rc%, 0, "update at full load should succeed") +AssertEqStr(HmGet$(sm, "k0"), "updated", "updated value at full load") +HmFree(sm) + +{* --- Test 11: Multiple independent instances --- *} +PRINT "--- Independent instances ---" +DECLARE CLASS Hashmap m1 +DECLARE CLASS Hashmap m2 +HmMake(m1, HM_SMALL) +HmMake(m2, HM_SMALL) + +rc% = HmPut$(m1, "x", "one") +rc% = HmPut$(m2, "x", "two") + +AssertEqStr(HmGet$(m1, "x"), "one", "m1 x is one") +AssertEqStr(HmGet$(m2, "x"), "two", "m2 x is two") + +rc% = HmDel(m1, "x") +AssertTrue(NOT HmHas(m1, "x"), "m1 x deleted") +AssertTrue(HmHas(m2, "x"), "m2 x still exists") + +HmFree(m1) +HmFree(m2) + +{* --- Test 12: Clear --- *} +PRINT "--- Clear ---" +DECLARE CLASS Hashmap mc +HmMake(mc, HM_SMALL) +rc% = HmPut$(mc, "a", "1") +rc% = HmPut$(mc, "b", "2") +HmClear(mc) +AssertEq&(HmCount(mc), 0, "count 0 after clear") +AssertTrue(NOT HmHas(mc, "a"), "a gone after clear") +AssertTrue(NOT HmHas(mc, "b"), "b gone after clear") + +' Can reuse after clear +rc% = HmPut$(mc, "c", "3") +AssertEqStr(HmGet$(mc, "c"), "3", "get c after clear+put") +HmFree(mc) + +{* --- Test 13: Empty string key and value --- *} +PRINT "--- Edge cases ---" +DECLARE CLASS Hashmap me +HmMake(me, HM_SMALL) +rc% = HmPut$(me, "", "empty key") +AssertEq%(rc%, 0, "put empty key should succeed") +AssertEqStr(HmGet$(me, ""), "empty key", "get empty key") +AssertTrue(HmHas(me, ""), "has empty key") + +rc% = HmPut$(me, "blank", "") +AssertEq%(rc%, 0, "put empty value should succeed") +AssertEqStr(HmGet$(me, "blank"), "", "get empty value") +AssertTrue(HmHas(me, "blank"), "has blank key with empty value") +HmFree(me) + +{* ============== Summary ============== *} +PRINT +PRINT "Results:"; _passed; " passed,"; _failed; " failed" +IF _failed > 0 THEN + PRINT "ASSERT FAILED: Some tests failed" +END IF diff --git a/submods/hashmap/test_foreach.b b/submods/hashmap/test_foreach.b new file mode 100644 index 0000000..ff3aa5c --- /dev/null +++ b/submods/hashmap/test_foreach.b @@ -0,0 +1,270 @@ +REM #using ace:submods/hashmap/hashmap.o + +{* +** Hashmap: HmForEach Tests +** Tests higher-order iteration with callback using BIND/INVOKE. +** Callback signature: (ADDRESS keyPtr, LONGINT rawVal&, +** ADDRESS strPtr, SHORTINT typ%) +*} + +#include + +SHORTINT _passed, _failed + +{* ============== Assertion Helpers ============== *} + +SUB AssertTrue(SHORTINT condition, msg$) + SHARED _passed, _failed + IF condition THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$ + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEqStr(actual$, expected$, msg$) + SHARED _passed, _failed + IF actual$ = expected$ THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected '"; expected$; "' got '"; actual$; "')" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq&(LONGINT actual, LONGINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq%(SHORTINT actual, SHORTINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +{* ============== Shared State for Callbacks ============== *} + +SHORTINT _feCount +STRING _feKeys$ SIZE 512 +LONGINT _feSum +SHORTINT _feStrCnt, _feLngCnt, _feSngCnt, _feBoolCnt, _feNullCnt +STRING _feLastKey$ SIZE 64 +STRING _feLastStr$ SIZE 256 + +{* ============== Callback Forward Declares ============== *} + +DECLARE SUB ADDRESS CountEntry(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + +DECLARE SUB ADDRESS CollectKeys(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + +DECLARE SUB ADDRESS SumLongs(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + +DECLARE SUB ADDRESS CountTypes(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + +DECLARE SUB ADDRESS CaptureStr(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + +{* ============== Test Suite ============== *} + +PRINT "=== Hashmap: HmForEach Tests ===" +PRINT + +_passed = 0 +_failed = 0 + +SHORTINT rc% + +{* --- Test 1: Empty map --- *} +PRINT "--- Empty map ---" +DECLARE CLASS Hashmap m1 +HmMake(m1, HM_SMALL) + +_feCount = 0 +HmForEach(m1, BIND(@CountEntry)) +AssertEq%(_feCount, 0, "count 0 on empty") + +HmFree(m1) + +{* --- Test 2: Count 3 entries --- *} +PRINT "--- Count 3 entries ---" +DECLARE CLASS Hashmap m2 +HmMake(m2, HM_SMALL) + +rc% = HmPut$(m2, "a", "A") +rc% = HmPut$(m2, "b", "B") +rc% = HmPut$(m2, "c", "C") + +_feCount = 0 +HmForEach(m2, BIND(@CountEntry)) +AssertEq%(_feCount, 3, "count 3 entries") + +HmFree(m2) + +{* --- Test 3: Keys in insertion order --- *} +PRINT "--- Key order ---" +DECLARE CLASS Hashmap m3 +HmMake(m3, HM_SMALL) + +rc% = HmPut$(m3, "alpha", "1") +rc% = HmPut$(m3, "beta", "2") +rc% = HmPut$(m3, "gamma", "3") + +_feKeys$ = "" +HmForEach(m3, BIND(@CollectKeys)) +AssertEqStr(_feKeys$, "alpha,beta,gamma", "keys in order") + +HmFree(m3) + +{* --- Test 4: Skips deleted entries --- *} +PRINT "--- After delete ---" +DECLARE CLASS Hashmap m4 +HmMake(m4, HM_SMALL) + +rc% = HmPut$(m4, "x", "1") +rc% = HmPut$(m4, "y", "2") +rc% = HmPut$(m4, "z", "3") +rc% = HmDel(m4, "y") + +_feCount = 0 +HmForEach(m4, BIND(@CountEntry)) +AssertEq%(_feCount, 2, "count 2 after del") + +_feKeys$ = "" +HmForEach(m4, BIND(@CollectKeys)) +AssertEqStr(_feKeys$, "x,z", "del: keys x,z (y skipped)") + +HmFree(m4) + +{* --- Test 5: Sum LONG values --- *} +PRINT "--- Sum LONGs ---" +DECLARE CLASS Hashmap m5 +HmMake(m5, HM_SMALL) + +rc% = HmPut&(m5, "a", 10) +rc% = HmPut&(m5, "b", 20) +rc% = HmPut&(m5, "c", 30) + +_feSum = 0 +HmForEach(m5, BIND(@SumLongs)) +AssertEq&(_feSum, 60, "sum is 60") + +HmFree(m5) + +{* --- Test 6: Mixed types counting --- *} +PRINT "--- Mixed type counting ---" +DECLARE CLASS Hashmap m6 +HmMake(m6, HM_SMALL) + +rc% = HmPut$(m6, "s1", "hello") +rc% = HmPut$(m6, "s2", "world") +rc% = HmPut&(m6, "n1", 42) +rc% = HmPut!(m6, "f1", 3.14) +rc% = HmPutBool(m6, "b1", -1) +rc% = HmPutNull(m6, "z1") + +_feStrCnt = 0 +_feLngCnt = 0 +_feSngCnt = 0 +_feBoolCnt = 0 +_feNullCnt = 0 +HmForEach(m6, BIND(@CountTypes)) +AssertEq%(_feStrCnt, 2, "2 strings") +AssertEq%(_feLngCnt, 1, "1 long") +AssertEq%(_feSngCnt, 1, "1 single") +AssertEq%(_feBoolCnt, 1, "1 bool") +AssertEq%(_feNullCnt, 1, "1 null") + +HmFree(m6) + +{* --- Test 7: String value accessible via CSTR --- *} +PRINT "--- String value access ---" +DECLARE CLASS Hashmap m7 +HmMake(m7, HM_SMALL) + +rc% = HmPut$(m7, "msg", "hello world") + +_feLastKey$ = "" +_feLastStr$ = "" +HmForEach(m7, BIND(@CaptureStr)) +AssertEqStr(_feLastKey$, "msg", "key via CSTR") +AssertEqStr(_feLastStr$, "hello world", "val via CSTR") + +HmFree(m7) + +{* ============== Summary ============== *} +PRINT +PRINT "Results:"; _passed; " passed,"; _failed; " failed" +IF _failed > 0 THEN + PRINT "ASSERT FAILED: Some tests failed" +END IF + +{* ============== Callback Implementations ============== *} + +{* Count entries (side effects only) *} +SUB ADDRESS CountEntry(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + SHARED _feCount + _feCount = _feCount + 1 + CountEntry = 0 +END SUB + +{* Collect keys into comma-separated string *} +SUB ADDRESS CollectKeys(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + SHARED _feKeys$ + IF LEN(_feKeys$) > 0 THEN _feKeys$ = _feKeys$ + "," + _feKeys$ = _feKeys$ + CSTR(keyPtr) + CollectKeys = 0 +END SUB + +{* Sum LONG rawVal values *} +SUB ADDRESS SumLongs(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + SHARED _feSum + _feSum = _feSum + rawVal& + SumLongs = 0 +END SUB + +{* Count entries by type *} +SUB ADDRESS CountTypes(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + SHARED _feStrCnt, _feLngCnt, _feSngCnt, _feBoolCnt, _feNullCnt + IF typ% = HmTypeStr THEN + _feStrCnt = _feStrCnt + 1 + ELSEIF typ% = HmTypeLng THEN + _feLngCnt = _feLngCnt + 1 + ELSEIF typ% = HmTypeSng THEN + _feSngCnt = _feSngCnt + 1 + ELSEIF typ% = HmTypeBool THEN + _feBoolCnt = _feBoolCnt + 1 + ELSEIF typ% = HmTypeNull THEN + _feNullCnt = _feNullCnt + 1 + END IF + CountTypes = 0 +END SUB + +{* Capture key and string value *} +SUB ADDRESS CaptureStr(ADDRESS keyPtr, LONGINT rawVal&, ~ + ADDRESS strPtr, SHORTINT typ%) INVOKABLE + SHARED _feLastKey$, _feLastStr$ + _feLastKey$ = CSTR(keyPtr) + IF typ% = HmTypeStr THEN + _feLastStr$ = CSTR(strPtr) + END IF + CaptureStr = 0 +END SUB diff --git a/submods/hashmap/test_iter.b b/submods/hashmap/test_iter.b new file mode 100644 index 0000000..6d3469f --- /dev/null +++ b/submods/hashmap/test_iter.b @@ -0,0 +1,439 @@ +REM #using ace:submods/hashmap/hashmap.o + +{* +** Hashmap Phase 3: Iteration Tests +** Tests insertion-order iteration, mixed types, iteration after +** deletes, tombstone reuse ordering, reset, independent instances. +*} + +#include + +SHORTINT _passed, _failed + +{* ============== Assertion Helpers ============== *} + +SUB AssertTrue(SHORTINT condition, msg$) + SHARED _passed, _failed + IF condition THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$ + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEqStr(actual$, expected$, msg$) + SHARED _passed, _failed + IF actual$ = expected$ THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected '"; expected$; "' got '"; actual$; "')" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq&(LONGINT actual, LONGINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq%(SHORTINT actual, SHORTINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +{* ============== Test Suite ============== *} + +PRINT "=== Hashmap Phase 3: Iteration Tests ===" +PRINT + +_passed = 0 +_failed = 0 + +SHORTINT rc%, ok% + +{* --- Test 1: Basic insertion order --- *} +PRINT "--- Basic insertion order ---" +DECLARE CLASS Hashmap m1 +HmMake(m1, HM_SMALL) + +rc% = HmPut$(m1, "alpha", "A") +rc% = HmPut$(m1, "beta", "B") +rc% = HmPut$(m1, "gamma", "C") + +HmIterReset(m1) + +ok% = HmIterNext(m1) +AssertTrue(ok%, "iter1 has 1st") +AssertEqStr(HmIterKey$(m1), "alpha", "iter1 1st key") +AssertEqStr(HmIterVal$(m1), "A", "iter1 1st val") + +ok% = HmIterNext(m1) +AssertTrue(ok%, "iter1 has 2nd") +AssertEqStr(HmIterKey$(m1), "beta", "iter1 2nd key") +AssertEqStr(HmIterVal$(m1), "B", "iter1 2nd val") + +ok% = HmIterNext(m1) +AssertTrue(ok%, "iter1 has 3rd") +AssertEqStr(HmIterKey$(m1), "gamma", "iter1 3rd key") +AssertEqStr(HmIterVal$(m1), "C", "iter1 3rd val") + +ok% = HmIterNext(m1) +AssertTrue(NOT ok%, "iter1 ends after 3") + +HmFree(m1) + +{* --- Test 2: Empty map iteration --- *} +PRINT "--- Empty map ---" +DECLARE CLASS Hashmap m2 +HmMake(m2, HM_SMALL) + +HmIterReset(m2) +ok% = HmIterNext(m2) +AssertTrue(NOT ok%, "empty map iter returns 0") + +HmFree(m2) + +{* --- Test 3: Iteration count matches HmCount --- *} +PRINT "--- Count match ---" +DECLARE CLASS Hashmap m3 +HmMake(m3, HM_SMALL) + +rc% = HmPut$(m3, "a", "1") +rc% = HmPut$(m3, "b", "2") +rc% = HmPut$(m3, "c", "3") +rc% = HmPut$(m3, "d", "4") +rc% = HmPut$(m3, "e", "5") + +SHORTINT iterCnt% +iterCnt% = 0 +HmIterReset(m3) +WHILE HmIterNext(m3) + iterCnt% = iterCnt% + 1 +WEND +AssertEq%(iterCnt%, 5, "iter count matches HmCount") + +HmFree(m3) + +{* --- Test 4: Iteration after delete --- *} +PRINT "--- After delete ---" +DECLARE CLASS Hashmap m4 +HmMake(m4, HM_SMALL) + +rc% = HmPut$(m4, "x", "1") +rc% = HmPut$(m4, "y", "2") +rc% = HmPut$(m4, "z", "3") + +' Delete middle entry +rc% = HmDel(m4, "y") + +HmIterReset(m4) + +ok% = HmIterNext(m4) +AssertTrue(ok%, "del iter has 1st") +AssertEqStr(HmIterKey$(m4), "x", "del iter 1st key") + +ok% = HmIterNext(m4) +AssertTrue(ok%, "del iter has 2nd") +AssertEqStr(HmIterKey$(m4), "z", "del iter 2nd key (y skipped)") + +ok% = HmIterNext(m4) +AssertTrue(NOT ok%, "del iter ends after 2") + +HmFree(m4) + +{* --- Test 5: Delete first and last --- *} +PRINT "--- Delete first and last ---" +DECLARE CLASS Hashmap m5 +HmMake(m5, HM_SMALL) + +rc% = HmPut$(m5, "first", "1") +rc% = HmPut$(m5, "mid", "2") +rc% = HmPut$(m5, "last", "3") + +rc% = HmDel(m5, "first") +rc% = HmDel(m5, "last") + +HmIterReset(m5) +ok% = HmIterNext(m5) +AssertTrue(ok%, "del-fl has 1 entry") +AssertEqStr(HmIterKey$(m5), "mid", "del-fl only mid remains") +ok% = HmIterNext(m5) +AssertTrue(NOT ok%, "del-fl ends after 1") + +HmFree(m5) + +{* --- Test 6: Tombstone reuse preserves order --- *} +PRINT "--- Tombstone reuse order ---" +DECLARE CLASS Hashmap m6 +HmMake(m6, HM_SMALL) + +rc% = HmPut$(m6, "aa", "1") +rc% = HmPut$(m6, "bb", "2") +rc% = HmPut$(m6, "cc", "3") + +' Delete bb, then insert dd (may reuse bb's slot) +rc% = HmDel(m6, "bb") +rc% = HmPut$(m6, "dd", "4") + +' Expected order: aa, cc, dd (bb deleted, dd appended) +HmIterReset(m6) + +ok% = HmIterNext(m6) +AssertEqStr(HmIterKey$(m6), "aa", "tomb order 1st = aa") +ok% = HmIterNext(m6) +AssertEqStr(HmIterKey$(m6), "cc", "tomb order 2nd = cc") +ok% = HmIterNext(m6) +AssertEqStr(HmIterKey$(m6), "dd", "tomb order 3rd = dd") +ok% = HmIterNext(m6) +AssertTrue(NOT ok%, "tomb order ends after 3") + +HmFree(m6) + +{* --- Test 7: Mixed types iteration --- *} +PRINT "--- Mixed types ---" +DECLARE CLASS Hashmap m7 +HmMake(m7, HM_SMALL) + +rc% = HmPut$(m7, "nm", "Alice") +rc% = HmPut&(m7, "ag", 30) +rc% = HmPut!(m7, "ht", 5.5) +rc% = HmPutBool(m7, "ok", -1) +rc% = HmPutNull(m7, "nn") + +SINGLE testF! + +HmIterReset(m7) + +ok% = HmIterNext(m7) +AssertEqStr(HmIterKey$(m7), "nm", "mix iter 1st key") +AssertEq%(HmIterType(m7), HmTypeStr, "mix iter 1st type") +AssertEqStr(HmIterVal$(m7), "Alice", "mix iter 1st val") + +ok% = HmIterNext(m7) +AssertEqStr(HmIterKey$(m7), "ag", "mix iter 2nd key") +AssertEq%(HmIterType(m7), HmTypeLng, "mix iter 2nd type") +AssertEq&(HmIterVal&(m7), 30, "mix iter 2nd val") + +ok% = HmIterNext(m7) +AssertEqStr(HmIterKey$(m7), "ht", "mix iter 3rd key") +AssertEq%(HmIterType(m7), HmTypeSng, "mix iter 3rd type") +testF! = HmIterVal!(m7) +AssertTrue(testF! > 5.4 AND testF! < 5.6, "mix iter 3rd val ~5.5") + +ok% = HmIterNext(m7) +AssertEqStr(HmIterKey$(m7), "ok", "mix iter 4th key") +AssertEq%(HmIterType(m7), HmTypeBool, "mix iter 4th type") +AssertEq&(HmIterVal&(m7), 1, "mix iter 4th val") + +ok% = HmIterNext(m7) +AssertEqStr(HmIterKey$(m7), "nn", "mix iter 5th key") +AssertEq%(HmIterType(m7), HmTypeNull, "mix iter 5th type") + +ok% = HmIterNext(m7) +AssertTrue(NOT ok%, "mix iter ends after 5") + +HmFree(m7) + +{* --- Test 8: Re-iteration (reset and iterate again) --- *} +PRINT "--- Re-iteration ---" +DECLARE CLASS Hashmap m8 +HmMake(m8, HM_SMALL) + +rc% = HmPut$(m8, "p", "1") +rc% = HmPut$(m8, "q", "2") + +' First pass +HmIterReset(m8) +ok% = HmIterNext(m8) +AssertEqStr(HmIterKey$(m8), "p", "pass1 1st") +ok% = HmIterNext(m8) +AssertEqStr(HmIterKey$(m8), "q", "pass1 2nd") + +' Second pass - same results +HmIterReset(m8) +ok% = HmIterNext(m8) +AssertEqStr(HmIterKey$(m8), "p", "pass2 1st") +ok% = HmIterNext(m8) +AssertEqStr(HmIterKey$(m8), "q", "pass2 2nd") +ok% = HmIterNext(m8) +AssertTrue(NOT ok%, "pass2 ends") + +HmFree(m8) + +{* --- Test 9: Independent instance iteration --- *} +PRINT "--- Independent instances ---" +DECLARE CLASS Hashmap ia +DECLARE CLASS Hashmap ib +HmMake(ia, HM_SMALL) +HmMake(ib, HM_SMALL) + +rc% = HmPut$(ia, "a1", "v1") +rc% = HmPut$(ia, "a2", "v2") + +rc% = HmPut$(ib, "b1", "w1") +rc% = HmPut$(ib, "b2", "w2") +rc% = HmPut$(ib, "b3", "w3") + +' Iterate ia +SHORTINT cntA% +cntA% = 0 +HmIterReset(ia) +WHILE HmIterNext(ia) + cntA% = cntA% + 1 +WEND +AssertEq%(cntA%, 2, "ia has 2 entries") + +' Iterate ib +SHORTINT cntB% +cntB% = 0 +HmIterReset(ib) +WHILE HmIterNext(ib) + cntB% = cntB% + 1 +WEND +AssertEq%(cntB%, 3, "ib has 3 entries") + +' Verify ia order +HmIterReset(ia) +ok% = HmIterNext(ia) +AssertEqStr(HmIterKey$(ia), "a1", "ia 1st key") +ok% = HmIterNext(ia) +AssertEqStr(HmIterKey$(ia), "a2", "ia 2nd key") + +' Verify ib order +HmIterReset(ib) +ok% = HmIterNext(ib) +AssertEqStr(HmIterKey$(ib), "b1", "ib 1st key") +ok% = HmIterNext(ib) +AssertEqStr(HmIterKey$(ib), "b2", "ib 2nd key") +ok% = HmIterNext(ib) +AssertEqStr(HmIterKey$(ib), "b3", "ib 3rd key") + +HmFree(ia) +HmFree(ib) + +{* --- Test 10: HmClear resets iteration order --- *} +PRINT "--- Clear resets order ---" +DECLARE CLASS Hashmap mc +HmMake(mc, HM_SMALL) + +rc% = HmPut$(mc, "old1", "v1") +rc% = HmPut$(mc, "old2", "v2") + +HmClear(mc) + +rc% = HmPut$(mc, "new1", "w1") +rc% = HmPut$(mc, "new2", "w2") + +HmIterReset(mc) +ok% = HmIterNext(mc) +AssertEqStr(HmIterKey$(mc), "new1", "clear: 1st is new1") +ok% = HmIterNext(mc) +AssertEqStr(HmIterKey$(mc), "new2", "clear: 2nd is new2") +ok% = HmIterNext(mc) +AssertTrue(NOT ok%, "clear: ends after 2") + +HmFree(mc) + +{* --- Test 11: Update does not change order --- *} +PRINT "--- Update keeps order ---" +DECLARE CLASS Hashmap mu +HmMake(mu, HM_SMALL) + +rc% = HmPut$(mu, "k1", "v1") +rc% = HmPut$(mu, "k2", "v2") +rc% = HmPut$(mu, "k3", "v3") + +' Update k2 - should NOT change its position +rc% = HmPut$(mu, "k2", "updated") + +HmIterReset(mu) +ok% = HmIterNext(mu) +AssertEqStr(HmIterKey$(mu), "k1", "upd order 1st = k1") +ok% = HmIterNext(mu) +AssertEqStr(HmIterKey$(mu), "k2", "upd order 2nd = k2") +AssertEqStr(HmIterVal$(mu), "updated", "upd k2 has new value") +ok% = HmIterNext(mu) +AssertEqStr(HmIterKey$(mu), "k3", "upd order 3rd = k3") +ok% = HmIterNext(mu) +AssertTrue(NOT ok%, "upd order ends") + +HmFree(mu) + +{* --- Test 12: Many inserts iteration --- *} +PRINT "--- Many inserts ---" +DECLARE CLASS Hashmap mm +HmMake(mm, HM_SMALL) + +SHORTINT j% +STRING kk$ +FOR j% = 0 TO 19 + kk$ = "item" + LTRIM$(STR$(j%)) + rc% = HmPut&(mm, kk$, j%) +NEXT + +' Verify iteration count +SHORTINT mCnt% +mCnt% = 0 +HmIterReset(mm) +WHILE HmIterNext(mm) + mCnt% = mCnt% + 1 +WEND +AssertEq%(mCnt%, 20, "many: 20 entries iterated") + +' Verify first and last +HmIterReset(mm) +ok% = HmIterNext(mm) +AssertEqStr(HmIterKey$(mm), "item0", "many: 1st is item0") +AssertEq&(HmIterVal&(mm), 0, "many: 1st val is 0") + +' Walk to last +FOR j% = 1 TO 19 + ok% = HmIterNext(mm) +NEXT +AssertEqStr(HmIterKey$(mm), "item19", "many: last is item19") +AssertEq&(HmIterVal&(mm), 19, "many: last val is 19") + +HmFree(mm) + +{* --- Test 13: Ref iteration --- *} +PRINT "--- Ref iteration ---" +DECLARE CLASS Hashmap mr +DECLARE CLASS Hashmap inner +HmMake(mr, HM_SMALL) +HmMake(inner, HM_SMALL) + +rc% = HmPut$(inner, "foo", "bar") +rc% = HmPut$(mr, "label", "test") +rc% = HmPutRef(mr, "child", inner) + +HmIterReset(mr) +ok% = HmIterNext(mr) +AssertEqStr(HmIterKey$(mr), "label", "ref iter 1st key") +AssertEq%(HmIterType(mr), HmTypeStr, "ref iter 1st type") + +ok% = HmIterNext(mr) +AssertEqStr(HmIterKey$(mr), "child", "ref iter 2nd key") +AssertEq%(HmIterType(mr), HmTypeRef, "ref iter 2nd type") +AssertTrue(HmIterVal&(mr) <> 0, "ref iter 2nd val non-zero") +AssertEq&(HmIterVal&(mr), inner, "ref iter 2nd val = inner addr") + +HmFree(inner) +HmFree(mr) + +{* ============== Summary ============== *} +PRINT +PRINT "Results:"; _passed; " passed,"; _failed; " failed" +IF _failed > 0 THEN + PRINT "ASSERT FAILED: Some tests failed" +END IF diff --git a/submods/hashmap/test_typed.b b/submods/hashmap/test_typed.b new file mode 100644 index 0000000..9fee9de --- /dev/null +++ b/submods/hashmap/test_typed.b @@ -0,0 +1,284 @@ +REM #using ace:submods/hashmap/hashmap.o + +{* +** Hashmap Phase 2: Typed Value Tests +** Tests put/get for LONG, SINGLE, Bool, Null, Ref types. +** Tests HmType, type overwrite, mixed types, nested hashmap refs. +*} + +#include + +SHORTINT _passed, _failed + +{* ============== Assertion Helpers ============== *} + +SUB AssertTrue(SHORTINT condition, msg$) + SHARED _passed, _failed + IF condition THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$ + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEqStr(actual$, expected$, msg$) + SHARED _passed, _failed + IF actual$ = expected$ THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected '"; expected$; "' got '"; actual$; "')" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq&(LONGINT actual, LONGINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +SUB AssertEq%(SHORTINT actual, SHORTINT expected, msg$) + SHARED _passed, _failed + IF actual = expected THEN + _passed = _passed + 1 + ELSE + PRINT "FAIL: "; msg$; " (expected"; expected; " got"; actual; ")" + _failed = _failed + 1 + END IF +END SUB + +{* ============== Test Suite ============== *} + +PRINT "=== Hashmap Phase 2: Typed Value Tests ===" +PRINT + +_passed = 0 +_failed = 0 + +SHORTINT rc% +DECLARE CLASS Hashmap m + +{* --- Test 1: Put & Get LONGINT --- *} +PRINT "--- Put / Get LONGINT ---" +HmMake(m, HM_SMALL) + +rc% = HmPut&(m, "age", 30) +AssertEq%(rc%, 0, "put& age should succeed") +AssertEq&(HmGet&(m, "age"), 30, "get& age") + +rc% = HmPut&(m, "year", 2026) +AssertEq%(rc%, 0, "put& year should succeed") +AssertEq&(HmGet&(m, "year"), 2026, "get& year") + +' Negative value +rc% = HmPut&(m, "neg", -42) +AssertEq%(rc%, 0, "put& negative should succeed") +AssertEq&(HmGet&(m, "neg"), -42, "get& negative") + +' Zero +rc% = HmPut&(m, "zero", 0) +AssertEq%(rc%, 0, "put& zero should succeed") +AssertEq&(HmGet&(m, "zero"), 0, "get& zero") + +' Get missing returns 0 +AssertEq&(HmGet&(m, "missing"), 0, "get& missing returns 0") + +HmFree(m) + +{* --- Test 2: Put & Get SINGLE --- *} +PRINT "--- Put / Get SINGLE ---" +DECLARE CLASS Hashmap ms +HmMake(ms, HM_SMALL) + +SINGLE testF! + +rc% = HmPut!(ms, "pi", 3.14) +AssertEq%(rc%, 0, "put! pi should succeed") +testF! = HmGet!(ms, "pi") +AssertTrue(testF! > 3.13 AND testF! < 3.15, "get! pi approx 3.14") + +rc% = HmPut!(ms, "half", 0.5) +AssertEq%(rc%, 0, "put! half should succeed") +testF! = HmGet!(ms, "half") +AssertTrue(testF! > 0.49 AND testF! < 0.51, "get! half approx 0.5") + +' Negative float +rc% = HmPut!(ms, "negf", -1.5) +AssertEq%(rc%, 0, "put! negf should succeed") +testF! = HmGet!(ms, "negf") +AssertTrue(testF! < -1.4 AND testF! > -1.6, "get! negf approx -1.5") + +HmFree(ms) + +{* --- Test 3: Put Bool --- *} +PRINT "--- Put Bool ---" +DECLARE CLASS Hashmap mb +HmMake(mb, HM_SMALL) + +rc% = HmPutBool(mb, "active", -1) +AssertEq%(rc%, 0, "putBool active should succeed") +AssertEq&(HmGet&(mb, "active"), 1, "getBool active is 1") + +rc% = HmPutBool(mb, "deleted", 0) +AssertEq%(rc%, 0, "putBool deleted should succeed") +AssertEq&(HmGet&(mb, "deleted"), 0, "getBool deleted is 0") + +HmFree(mb) + +{* --- Test 4: Put Null --- *} +PRINT "--- Put Null ---" +DECLARE CLASS Hashmap mn +HmMake(mn, HM_SMALL) + +rc% = HmPutNull(mn, "nothing") +AssertEq%(rc%, 0, "putNull should succeed") +AssertTrue(HmHas(mn, "nothing"), "has null key") +AssertEq%(HmType(mn, "nothing"), HmTypeNull, "type is null") + +HmFree(mn) + +{* --- Test 5: HmType for all types --- *} +PRINT "--- HmType ---" +DECLARE CLASS Hashmap mt +HmMake(mt, HM_SMALL) + +rc% = HmPut$(mt, "s", "hello") +AssertEq%(HmType(mt, "s"), HmTypeStr, "type str") + +rc% = HmPut&(mt, "l", 100) +AssertEq%(HmType(mt, "l"), HmTypeLng, "type lng") + +rc% = HmPut!(mt, "f", 1.5) +AssertEq%(HmType(mt, "f"), HmTypeSng, "type sng") + +rc% = HmPutBool(mt, "b", -1) +AssertEq%(HmType(mt, "b"), HmTypeBool, "type bool") + +rc% = HmPutNull(mt, "n") +AssertEq%(HmType(mt, "n"), HmTypeNull, "type null") + +' Missing key returns -1 +AssertEq%(HmType(mt, "nope"), -1, "type missing returns -1") + +HmFree(mt) + +{* --- Test 6: Type overwrite --- *} +PRINT "--- Type overwrite ---" +DECLARE CLASS Hashmap mo +HmMake(mo, HM_SMALL) + +' Start as string +rc% = HmPut$(mo, "x", "hello") +AssertEq%(HmType(mo, "x"), HmTypeStr, "x initially str") +AssertEqStr(HmGet$(mo, "x"), "hello", "x str value") + +' Overwrite with LONGINT +rc% = HmPut&(mo, "x", 99) +AssertEq%(HmType(mo, "x"), HmTypeLng, "x now lng") +AssertEq&(HmGet&(mo, "x"), 99, "x lng value") + +' Overwrite with null +rc% = HmPutNull(mo, "x") +AssertEq%(HmType(mo, "x"), HmTypeNull, "x now null") + +' Count unchanged through overwrites +AssertEq&(HmCount(mo), 1, "count still 1 after overwrites") + +HmFree(mo) + +{* --- Test 7: Mixed types in same map --- *} +PRINT "--- Mixed types ---" +DECLARE CLASS Hashmap mx +HmMake(mx, HM_SMALL) + +rc% = HmPut$(mx, "nm", "Alice") +rc% = HmPut&(mx, "ag", 30) +rc% = HmPut!(mx, "ht", 5.5) +rc% = HmPutBool(mx, "ok", -1) +rc% = HmPutNull(mx, "nn") + +AssertEq&(HmCount(mx), 5, "5 mixed entries") + +AssertEqStr(HmGet$(mx, "nm"), "Alice", "mixed get str") +AssertEq&(HmGet&(mx, "ag"), 30, "mixed get lng") +testF! = HmGet!(mx, "ht") +AssertTrue(testF! > 5.4 AND testF! < 5.6, "mixed get sng") +AssertEq&(HmGet&(mx, "ok"), 1, "mixed get bool") +AssertEq%(HmType(mx, "nn"), HmTypeNull, "mixed type null") + +HmFree(mx) + +{* --- Test 8: PutRef / GetRef with nested Hashmap --- *} +PRINT "--- Ref (nested hashmap) ---" +DECLARE CLASS Hashmap outer +DECLARE CLASS Hashmap inner +HmMake(outer, HM_SMALL) +HmMake(inner, HM_SMALL) + +' Put data in inner map +rc% = HmPut$(inner, "street", "123 Main St") + +' Store inner map as ref in outer map +rc% = HmPutRef(outer, "addr", inner) +AssertEq%(rc%, 0, "putRef should succeed") +AssertEq%(HmType(outer, "addr"), HmTypeRef, "type is ref") + +' Retrieve the ref and verify it points to inner +LONGINT refAddr& +refAddr& = HmGetRef(outer, "addr") +AssertTrue(refAddr& <> 0, "ref is non-zero") +AssertEq&(refAddr&, inner, "ref equals inner address") + +' Use the retrieved ref as a Hashmap +' (Verify inner map data is intact) +AssertEqStr(HmGet$(inner, "street"), "123 Main St", "inner data intact") + +HmFree(inner) +HmFree(outer) + +{* --- Test 9: Update LONGINT value --- *} +PRINT "--- Update typed value ---" +DECLARE CLASS Hashmap mu +HmMake(mu, HM_SMALL) + +rc% = HmPut&(mu, "count", 10) +AssertEq&(HmGet&(mu, "count"), 10, "initial count") + +rc% = HmPut&(mu, "count", 20) +AssertEq&(HmGet&(mu, "count"), 20, "updated count") +AssertEq&(HmCount(mu), 1, "count unchanged after update") + +HmFree(mu) + +{* --- Test 10: Delete typed entries --- *} +PRINT "--- Delete typed entries ---" +DECLARE CLASS Hashmap md +HmMake(md, HM_SMALL) + +rc% = HmPut&(md, "a", 1) +rc% = HmPut!(md, "b", 2.5) +rc% = HmPutBool(md, "c", -1) +AssertEq&(HmCount(md), 3, "3 entries before delete") + +rc% = HmDel(md, "b") +AssertEq%(rc%, 0, "del typed entry succeeds") +AssertTrue(NOT HmHas(md, "b"), "b gone after del") +AssertEq&(HmCount(md), 2, "count after del") + +' Remaining entries still accessible +AssertEq&(HmGet&(md, "a"), 1, "a still accessible") +AssertEq&(HmGet&(md, "c"), 1, "c still accessible") + +HmFree(md) + +{* ============== Summary ============== *} +PRINT +PRINT "Results:"; _passed; " passed,"; _failed; " failed" +IF _failed > 0 THEN + PRINT "ASSERT FAILED: Some tests failed" +END IF diff --git a/submods/httpclient/README.txt b/submods/httpclient/README.txt index 99ff790..156c0b4 100644 --- a/submods/httpclient/README.txt +++ b/submods/httpclient/README.txt @@ -1,6 +1,8 @@ HTTP/1.1 Client Library for ACE BASIC ====================================== +Designed by Manfred Bergmann, copyright 2026. + An HTTP/1.1 client submodule for ACE BASIC with optional HTTPS support via AmiSSL. Provides three API tiers for different use cases. diff --git a/submods/list/README.txt b/submods/list/README.txt index efb0b71..1dd98d3 100644 --- a/submods/list/README.txt +++ b/submods/list/README.txt @@ -1,6 +1,8 @@ Lisp-style List Library for ACE BASIC ====================================== +Designed by Manfred Bergmann, copyright 2026. + A singly-linked list implementation inspired by Common Lisp, providing type-safe cons cells with support for multiple data types. diff --git a/submods/sagasound/README.txt b/submods/sagasound/README.txt new file mode 100644 index 0000000..6a74271 --- /dev/null +++ b/submods/sagasound/README.txt @@ -0,0 +1,4 @@ +SAGA Sound Submodule for ACE BASIC +================================== + +Designed by Manfred Bergmann, copyright 2026. diff --git a/submods/tcpclient/Readme.txt b/submods/tcpclient/Readme.txt index a65b12e..c008f12 100644 --- a/submods/tcpclient/Readme.txt +++ b/submods/tcpclient/Readme.txt @@ -1,6 +1,8 @@ TCPClient - TCP Connection Submodule for ACE BASIC ================================================== +Designed by Manfred Bergmann, copyright 2026. + A struct-based TCP client library with optional SSL/TLS support. The caller owns TcpConn structs, so there is no connection limit. diff --git a/verify/scripts/otherthenamiga/call-on-ustartup b/verify/scripts/otherthenamiga/call-on-ustartup index 8b13789..1d2ccc0 100644 --- a/verify/scripts/otherthenamiga/call-on-ustartup +++ b/verify/scripts/otherthenamiga/call-on-ustartup @@ -1 +1,5 @@ +cd ace:submods/hashmap +execute ACE:bin/bas -m hashmap >ace:build-output.txt +bas test_iter >>ace:build-output.txt +test_iter >ace:test-output.txt