Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions include/submods/hashmap.h
Original file line number Diff line number Diff line change
@@ -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 <submods/hashmap.h>
** 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
139 changes: 139 additions & 0 deletions specs/hashmap-submod-state.txt
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 4 additions & 0 deletions submods/amissl/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AmiSSL Submodule for ACE BASIC
==============================

Designed by Manfred Bergmann, copyright 2026.
Loading