;license:MIT ;(c) 2018-9 by 4am ; ; Ordered key/value store (6502 compatible) ; ; Public functions ; - okvs_init(address) reset (required) ; - okvs_len(address) get number of keys ; - okvs_append(address, key, value, max_len) add new key/value pair ; - okvs_update(address, key, value) update key/value pair ; - okvs_get(address, key) get value by key lookup ; - okvs_nth(address, n) get key by numeric index ; - okvs_next(address, n) get next key by numeric index ; - okvs_iter(address, callback) iterate through keys ; - okvs_iter_values(address, callback) iterate through values ; ; Call init() once. Call it again to reset the store to 0 keys. ; ; Keys are maintained in a singly linked list, so most functions are O(n). ; len() and append() are O(1) though. ; ; Key count is stored as a byte, so a store can hold a maximum of 255 keys. ; ; Keys and values are length-prefixed strings (Pascal style), so max length ; of any single key or value is 255 bytes. ; ; Keys are case-sensitive. Lookups are an exact byte-for-byte comparison. ; ; append() has a max_len argument to reserve more space for the value, in case ; you want to update it later. max_len is the total space to reserve, not the ; additional space. One exception: max_len can be 0, and it will be treated as ; length(value) at append time. update() always modifies the value in place. ; There is no range checking because this is assembly. ; All functions take the starting address of the store's data buffer in ; memory, so there can be multiple independent stores at one time. append() ; will happily extend the store's data buffer without limit. There is no ; overflow protection because this is assembly. ; ; There is no sort() function. ; ; There is no delete() function. ; ; Keys can be duplicated, but get() will always return the one that was ; append()ed first. ; ; Structures: ; ; Store ; +0 length (byte) ; +1 free space pointer after last record (word) ; +3 Record ; ...Record... ; ...Record... ; ; Record ; +0 next-record pointer (word) ; +2 key length ; +3 key ; +K+2 value length (actual length, not max length) ; +K+3 value ; ... filler bytes up to value max length (set at append() time) ;------------------------------------------------------------------------------ ; okvs_init ; ; in: A/Y = handle to storage space ; out: $00/$01 clobbered ; $02/$03 clobbered ; all registers clobbered ;------------------------------------------------------------------------------ okvs_init jsr GetStoreAddressFromAY ; PTR -> store ; Y = 0 tya sta (PTR),y ; set number of keys in store to 0 iny ; set next-free-space pointer to store + 3 ldx PTR+1 lda PTR clc adc #$03 bcc + inx + sta (PTR),y iny txa sta (PTR),y rts ;------------------------------------------------------------------------------ ; okvs_len ; ; in: A/Y = handle to storage space ; out: A contains number of keys in this store ; X preserved ; Y clobbered ; $00/$01 clobbered ; $02/$03 clobbered ;------------------------------------------------------------------------------ okvs_len jsr GetStoreAddressFromAY ; PTR -> store ; Y = 0 lda (PTR),y ; A = number of keys in store rts ;------------------------------------------------------------------------------ ; okvs_append ; ; in: stack contains 7 bytes of parameters: ; +1 [word] handle to storage space ; +3 [word] address of key ; +5 [word] address of value ; +7 [byte] maximum length of value (or 0 to fit) ; out: X = new number of records (same as calling okvs_len after okvs_append) ; A/Y clobbered ; $00/$01 clobbered ; $02/$03 clobbered ; $04/$05 has the address of the next available byte after the new record ; $08/$09 clobbered ;------------------------------------------------------------------------------ okvs_append +PARAMS_ON_STACK 7 jsr GetStoreAddress ; PTR -> store ; Y = 0 lda (PTR),y ; A = number of keys in store clc adc #1 pha ; will return this later sta (PTR),y ; increment number of keys iny lda (PTR),y ; get address of next free space tax iny lda (PTR),y sta PTR+1 sta SAVE+1 stx PTR stx SAVE ; PTR -> new record ; SAVE -> new record jsr incptr2 ; PTR -> space for new key +LDPARAM 3 +STAY SRC ; SRC -> new key to copy ldy #0 lda (SRC),y tay iny sty KEYLEN - dey ; copy new key lda (SRC),y sta (PTR),y cpy #$FF bne - lda PTR ; update PTR to byte after copied key clc adc KEYLEN sta PTR bcc + inc PTR+1 + ; PTR -> space for new value +LDPARAM 5 +STAY SRC ; SRC -> new value to copy ldy #7 lda (PARAM),y ; get max length of value tay bne + lda (SRC),y ; no max, use actual length instead tay iny + sty VALUELEN - dey lda (SRC),y sta (PTR),y cpy #$FF bne - lda PTR clc adc VALUELEN sta SRC bcc + inc PTR+1 + lda PTR+1 sta SRC+1 ; SRC -> byte after this record +LDAY SAVE +STAY PTR ; PTR -> this record again ldy #0 lda SRC ; update next-record pointer sta (PTR),y iny lda SRC+1 sta (PTR),y pla tax ; X = new OKVS length jsr GetStoreAddress ; PTR -> store ldy #2 - lda SRC-1,y sta (PTR),y ; update next-free-space pointer in head dey bne - rts ;------------------------------------------------------------------------------ ; okvs_get ; ; in: stack contains 4 bytes of parameters: ; +1 [word] handle to storage space ; +3 [word] address of key ; out: if C clear, key was found ; A/Y = lo/hi address of value ; X = numeric index of key ; if C set, key was not found and all registers are clobbered ; all other flags clobbered ; $00/$01 clobbered ; $02/$03 clobbered ; $04/$05 clobbered ;------------------------------------------------------------------------------ okvs_get +PARAMS_ON_STACK 4 jsr GetStoreAddress ; PTR -> store ; Y = 0 lda (PTR),y ; A = number of keys in store beq @fail ; no keys, fail immediately sta MAX jsr incptr3 ; PTR -> first record ldx #0 +LDPARAM 3 +STAY SRC ; SRC -> key we want to find ldy #0 lda (SRC),y tay iny sty KEYLEN @matchRecordLoop lda PTR+1 sta DEST+1 lda PTR clc adc #2 sta DEST bcc + inc DEST+1 ; DEST -> key of this record + ldy #0 @matchKeyLoop lda (SRC),y cmp (DEST),y bne @next iny cpy KEYLEN bne @matchKeyLoop +LDAY PTR clc adc KEYLEN bcc + iny + clc adc #2 sta PTR bcc + iny clc + rts @next jsr derefptr ; PTR -> next record inx cpx MAX bne @matchRecordLoop @fail sec rts okvs_get_current +STAY PTR ldy #0 lda (PTR),y clc adc PTR sta PTR bcc + inc PTR+1 + jmp incptr ;------------------------------------------------------------------------------ ; okvs_next ; get (N+1)th key, with wraparound ; ; in: A/Y = handle to storage space ; X = record index ; out: A/Y = lo/hi address of (X+1)th key, or first key if X was the last record ; X = next record index ; SAVE clobbered ; see okvs_nth for other exit conditions ;------------------------------------------------------------------------------ okvs_next +STAY PARAM inx stx SAVE jsr okvs_len eor SAVE bne + tax + +LDAY PARAM ; execution falls through here ;------------------------------------------------------------------------------ ; okvs_nth ; get (N)th key ; ; in: A/Y = handle to storage space ; X = record index ; out: A/Y = lo/hi address of nth key ; X preserved ; Z = 0 ; all other flags clobbered ; PTR clobbered ;------------------------------------------------------------------------------ okvs_nth jsr GetStoreAddressFromAY ; PTR -> store jsr incptr3 ; PTR -> first record txa beq @found pha - jsr derefptr dex bne - pla tax @found jsr incptr2 +LDAY PTR rts ;------------------------------------------------------------------------------ ; okvs_update ; ; in: stack contains 6 bytes of parameters: ; +1 [word] handle to storage space ; +3 [word] address of key ; +5 [word] address of new value ; out: if C clear, key was found and value was updated ; if C set, key was not found ; all registers are clobbered ; all other flags clobbered ; $00/$01 clobbered ; $02/$03 clobbered ; $04/$05 clobbered ;------------------------------------------------------------------------------ okvs_update +PARAMS_ON_STACK 6 ldy #6 lda (PARAM),y sta SAVE+1 dey lda (PARAM),y sta SAVE dey - lda (PARAM),y sta @getparams,y dey bne - jsr okvs_get @getparams=*-1 !word $FDFD ; SMC !word $FDFD ; SMC bcs @exit +STAY DEST ldy #0 lda (SAVE),y tay - lda (SAVE),y sta (DEST),y dey cpy #$FF bne - clc @exit rts ;------------------------------------------------------------------------------ ; okvs_iter / okvs_iter_values ; ; in: stack contains 4 bytes of parameters: ; +1 [word] handle to storage space ; +3 [word] address of callback ; out: will be called for each record in the store, in order, with ; X = numeric index of record ; A/Y = address of key or value (depends on which entry point you call) ; all registers are clobbered ; all flags clobbered ; PARAM clobbered ; PTR clobbered ;------------------------------------------------------------------------------ okvs_iter lda #$D0 ; 'BNE' opcode +HIDE_NEXT_2_BYTES okvs_iter_values lda #$24 ; 'BIT' opcode sta @branch +PARAMS_ON_STACK 4 jsr GetStoreAddress ; PTR -> store ; Y = 0 lda (PTR),y ; A = number of keys in store beq @exit ; no keys, exit immediately sta @max +LDPARAM 3 +STAY @callback jsr incptr3 ; PTR -> first record ldx #0 @loop lda #2 ; for iter, skip length = 2 @branch bne + ; SMC (iter_values puts a BIT here, so no branch) ; for iter_values, skip length = length(key) + 2 + 1 ldy #2 lda (PTR),y ; A = length of key clc adc #3 ; skip over pointer to next record (2 bytes) + key length (1 byte) + sta @skiplen txa pha ; save X on stack lda PTR+1 tay pha lda PTR pha ; save PTR on stack (in case callback clobbers it) clc @skiplen=*+1 adc #$FD ; SMC skip over pointer (and possibly key) bcc + iny ; A/Y -> value + @callback=*+1 jsr $FDFD ; SMC pla sta PTR pla sta PTR+1 ; restore PTR from stack pla tax ; restore X from stack jsr derefptr ; PTR -> next record inx @max=*+1 cpx #$FD ; SMC bne @loop @exit rts ;------------------------------------------------------------------------------ ; internal functions incptr3 jsr incptr incptr2 jsr incptr incptr ; preserves A, X, and Y inc PTR bne + inc PTR+1 + rts GetStoreAddressFromAY +STAY PTR jmp derefptr GetStoreAddress ; in: PARAM = address of stack params (any PARAMS_ON_STACK macro will do this) ; out: PTR = address of store (always the first parameter on stack) ; preserves X ldy #1 lda (PARAM),y sta PTR iny lda (PARAM),y sta PTR+1 ; PTR -> first parameter on stack ; execution falls through here derefptr ; out: Y = 0 ; preserves X ldy #1 lda (PTR),y pha dey lda (PTR),y sta PTR pla sta PTR+1 rts