4cade/src/okvs.a

451 lines
14 KiB
Plaintext
Raw Permalink Normal View History

;license:MIT
2021-10-08 06:28:11 +00:00
;(c) 2018-2021 by 4am & qkumba
;
2020-03-24 20:30:14 +00:00
; Ordered key/value store (6502 compatible)(256+ records compatible)
;
; Public functions
; - okvs_len(address) get number of keys
; - okvs_update(address, key, value) update key/value pair
; - okvs_get(address, key) get value by key lookup
; - okvs_find(address, key) get key by key lookup
; - okvs_nth(address, n) get key by numeric index
2019-09-16 16:36:10 +00:00
; - 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
;
2020-03-24 20:30:14 +00:00
; Call init() once. Call it again to reset the store to 0 records.
;
2020-03-24 20:30:14 +00:00
; Records are maintained in a singly linked list, so most functions are O(n).
2021-11-16 18:34:57 +00:00
; len() is always O(1). okvs_nth() can be O(1) if some other code produces
; a key lookup table and stores its address in the 'free space pointer
; after last record' field in the header.
;
2020-03-24 20:30:14 +00:00
; Record count is stored as a word, so a store can hold 65535 records.
;
; Keys and values are length-prefixed strings (Pascal style), so max length
2021-11-16 18:34:57 +00:00
; of any single key or value is 255 bytes. Record length is stored as a byte
; that includes itself, so the maximum data length of a record is 254 bytes.
;
; Keys are case-sensitive. Lookups are an exact byte-for-byte comparison.
;
; All functions take the starting address of the store's data buffer in
; memory, so there can be multiple independent stores at one time. Only the
2021-10-28 05:26:02 +00:00
; size of a record is stored, so stores are easily relocatable. There is no
; overflow protection because this is assembly.
;
2021-11-16 18:34:57 +00:00
; There is an append() function but it has been separated out of this version
; because it is only used once during program startup, so it does not need
; to persist in LC RAM.
;
; There is no sort() function.
;
; There is no delete() function.
;
2021-10-28 05:26:02 +00:00
; Keys can be duplicated, but get() and find() will always return the first
; match.
;
2021-11-16 18:34:57 +00:00
; Records can technically have more fields than key and value, but callers
; are on their own for navigating inside the record. If you have a pointer
; to one length-prefixed field (including key or value), okvs_next_field()
; will find the start of the next field.
;
; Structures:
;
; Store
2020-03-24 20:30:14 +00:00
; +0 number of records (word)
2021-11-16 18:34:57 +00:00
; +2 free space pointer after last record (word) or $0000
2020-03-24 20:30:14 +00:00
; +4 Record
; ...Record...
; ...Record...
;
; Record
; +0 record length (including this field)
; +1 key length
; +2 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_len
2021-10-14 19:08:50 +00:00
; okvs_len_imm
;
2019-09-20 23:42:26 +00:00
; in: A/Y = handle to storage space
2020-03-24 20:30:14 +00:00
; out: $WCOUNT contains number of keys in this store
2019-09-20 23:42:26 +00:00
; X preserved
2020-03-24 20:30:14 +00:00
; A, Y clobbered
2021-10-14 19:08:50 +00:00
; Z set if no keys
; $00/$01 clobbered
; $02/$03 clobbered
;------------------------------------------------------------------------------
okvs_len
2019-09-20 23:42:26 +00:00
jsr GetStoreAddressFromAY
2019-09-10 13:37:19 +00:00
; PTR -> store
; Y = 0
2021-10-14 19:08:50 +00:00
okvs_len_imm
2020-03-24 20:30:14 +00:00
lda (PTR), y ; get number of keys in store (word)
sta WCOUNT
iny
lda (PTR), y
sta WCOUNT+1
2021-10-14 19:08:50 +00:00
ora WCOUNT
rts
;------------------------------------------------------------------------------
; okvs_find / okvs_get
;
; in: stack contains 4 bytes of parameters:
; +1 [word] handle to storage space
; +3 [word] address of key
; out: if C clear, record was found
; A/Y = lo/hi address of key (okvs_find) or value (okvs_get)
2020-03-24 20:30:14 +00:00
; $WINDEX = index of found record (word)
2020-11-09 20:22:38 +00:00
; if C set, keyrecord was not found and X/Y are clobbered, A=0
; all other flags clobbered
; $00/$01 clobbered
; $02/$03 clobbered
; $04/$05 clobbered
;------------------------------------------------------------------------------
okvs_find
lda #$60
+HIDE_NEXT_2_BYTES
okvs_get
lda #$EA
sta @maybeGetValue
+PARAMS_ON_STACK 4
2019-09-10 13:37:19 +00:00
jsr GetStoreAddress
; PTR -> store
; Y = 0
2021-10-14 19:08:50 +00:00
jsr okvs_len_imm
beq @fail ; no keys, fail immediately
2020-03-24 20:30:14 +00:00
jsr incptr4
; PTR -> first record
2019-11-27 21:51:43 +00:00
+LDPARAMPTR 3, SRC ; SRC -> key we want to find
ldy #0
2020-03-24 20:30:14 +00:00
sty WINDEX
sty WINDEX+1
lda (SRC),y
tay
iny
sty KEYLEN
@matchRecordLoop
2020-11-09 20:22:38 +00:00
+LD16 PTR
tax
inx
bne +
iny
+ stx DEST ; DEST -> key of this record
sty DEST+1
2020-11-09 20:22:38 +00:00
ldy #0
@matchKeyLoop
lda (SRC),y
cmp (DEST),y
bne @next
iny
2019-10-10 01:02:46 +00:00
KEYLEN = *+1
cpy #$D1 ; SMC
bne @matchKeyLoop
2020-03-24 20:30:14 +00:00
+LD16 DEST
clc
@maybeGetValue
brk ; SMC
jsr okvs_next_field
2020-03-24 20:30:14 +00:00
+LD16 PTR
clc
rts
@next jsr stepptr ; PTR -> next record
2020-03-24 20:30:14 +00:00
inc WINDEX
bne +
inc WINDEX+1
+
lda WINDEX
cmp WCOUNT
bne @matchRecordLoop
lda WINDEX+1
2020-11-09 20:22:38 +00:00
eor WCOUNT+1
bne @matchRecordLoop
@fail sec
rts
2019-09-16 16:36:10 +00:00
;------------------------------------------------------------------------------
; okvs_next
; get (N+1)th key, with wraparound
;
; in: A/Y = handle to storage space
2020-03-24 20:30:14 +00:00
; $WINDEX = record index (word)
; out: A/Y = lo/hi address of ($WINDEX+1)th key, or first key if $WINDEX was the last record
; $WINDEX = record index of next record
2019-09-20 23:42:26 +00:00
; see okvs_nth for other exit conditions
2019-09-16 16:36:10 +00:00
;------------------------------------------------------------------------------
okvs_next
2020-03-24 20:30:14 +00:00
+ST16 PARAM
inc WINDEX
bne +
inc WINDEX+1
+
2019-09-16 16:36:10 +00:00
jsr okvs_len
2020-03-24 20:30:14 +00:00
+LD16 WINDEX
2021-10-13 22:58:26 +00:00
+CMP16ADDR_NE WCOUNT, +
2020-03-24 20:30:14 +00:00
sta WINDEX
sta WINDEX+1
2019-09-23 17:14:27 +00:00
+
2020-03-24 20:30:14 +00:00
+LD16 PARAM
2019-10-08 17:19:20 +00:00
; /!\ execution falls through here to okvs_nth
2019-09-20 23:27:16 +00:00
;------------------------------------------------------------------------------
; okvs_nth
; get (N)th key
;
; in: A/Y = handle to storage space
2020-03-24 20:30:14 +00:00
; $WINDEX = record index
2019-09-20 23:27:16 +00:00
; out: A/Y = lo/hi address of nth key
2020-03-24 20:30:14 +00:00
; $WINDEX preserved
2019-09-20 23:27:16 +00:00
; all other flags clobbered
2020-03-24 20:30:14 +00:00
; $PTR clobbered
2019-09-20 23:27:16 +00:00
;------------------------------------------------------------------------------
okvs_nth
2021-11-16 18:34:57 +00:00
jsr GetStoreAddressFromAY ; PTR -> store
2020-03-24 20:30:14 +00:00
+LD16 WINDEX
+ST16 SAVE
2021-11-16 18:34:57 +00:00
jsr incptr2 ; PTR -> store+2
ldy #1
lda (PTR), y
beq @slowpath ; if no key lookup table, iterate through store
pha ; otherwise look up key address and return it
dey
lda (PTR), y
sta PTR
pla
sta PTR+1 ; PTR -> key lookup table
asl SAVE
rol SAVE+1 ; SAVE = WINDEX*2 (16-bit)
lda SAVE
clc
adc PTR
bcc +
clc
inc PTR+1
+ sta PTR
lda SAVE+1
adc PTR+1
sta PTR+1 ; PTR -> nth record key lookup table
lda (PTR), y
pha
iny
lda (PTR), y
tay
pla ; A/Y -> nth key in store
rts
@slowpath
jsr incptr2 ; PTR -> first record
bne @next ; always branches
- jsr stepptr
2020-03-24 20:30:14 +00:00
@next
lda SAVE
2020-11-09 20:22:38 +00:00
dec SAVE
tay
2019-09-20 23:27:16 +00:00
bne -
2020-03-24 20:30:14 +00:00
lda SAVE+1
2020-11-09 20:22:38 +00:00
dec SAVE+1
tay
2020-03-24 20:30:14 +00:00
bne -
jsr incptr
2020-03-24 20:30:14 +00:00
+LD16 PTR
2019-09-16 16:36:10 +00:00
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
2020-03-24 20:30:14 +00:00
+ST16 DEST
ldy #0
lda (SAVE),y
tay
- lda (SAVE),y
sta (DEST),y
dey
cpy #$FF
bne -
clc
@exit rts
;------------------------------------------------------------------------------
2019-09-10 04:54:52 +00:00
; okvs_iter / okvs_iter_values
;
; in: stack contains 4 bytes of parameters:
; +1 [word] handle to storage space
; +3 [word] address of callback
; out: <callback> will be called for each record in the store, in order, with
2020-03-24 20:30:14 +00:00
; $WINDEX = numeric index of record (word)
2019-09-10 04:54:52 +00:00
; A/Y = address of key or value (depends on which entry point you call)
; all registers are clobbered
2019-10-08 17:39:06 +00:00
; Z=1
; all other flags clobbered
; PARAM clobbered
; PTR clobbered
2020-03-24 20:30:14 +00:00
; WINDEX clobbered
; WCOUNT clobbered
;------------------------------------------------------------------------------
okvs_iter
2019-09-10 13:37:19 +00:00
lda #$D0 ; 'BNE' opcode
2019-09-10 04:54:52 +00:00
+HIDE_NEXT_2_BYTES
okvs_iter_values
2019-09-10 13:37:19 +00:00
lda #$24 ; 'BIT' opcode
2019-09-10 04:54:52 +00:00
sta @branch
+PARAMS_ON_STACK 4
2019-09-10 13:37:19 +00:00
jsr GetStoreAddress
; PTR -> store
; Y = 0
2021-10-14 19:08:50 +00:00
jsr okvs_len_imm
beq @exit ; no keys, exit immediately
2021-10-13 22:58:26 +00:00
+LDPARAMPTR 3, @callback
2020-03-24 20:30:14 +00:00
jsr incptr4
; PTR -> first record
2020-03-24 20:30:14 +00:00
lda #0
sta WINDEX
sta WINDEX+1
2019-09-10 04:54:52 +00:00
@loop
lda #1 ; for iter, skip length = 1
2019-09-10 13:37:19 +00:00
@branch bne + ; SMC (iter_values puts a BIT here, so no branch)
; for iter_values, skip length = length(key) + 1 + 1
tay ;;ldy #1
lda (PTR),y ; A = length of key
clc
adc #2 ; skip over record length (1 byte) + key length (1 byte)
2019-09-22 03:00:05 +00:00
+ sta @skiplen
2020-03-24 20:30:14 +00:00
lda WCOUNT+1 ; save WCOUNT on stack
pha
lda WCOUNT
pha
lda WINDEX+1 ; save WINDEX on stack
pha
lda WINDEX
pha
lda PTR+1
tay
pha
lda PTR
2020-03-24 20:30:14 +00:00
pha ; save PTR on stack
clc
2019-09-22 03:00:05 +00:00
@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
2020-03-24 20:30:14 +00:00
sta WINDEX
pla
sta WINDEX+1
pla
sta WCOUNT
pla
sta WCOUNT+1
jsr stepptr ; PTR -> next record
2020-03-24 20:30:14 +00:00
inc WINDEX
bne +
inc WINDEX+1
+
lda WINDEX
cmp WCOUNT
bne @loop
lda WINDEX+1
cmp WCOUNT+1
bne @loop
@exit rts
;------------------------------------------------------------------------------
; internal functions
okvs_next_field
2021-10-13 22:58:26 +00:00
; out: Y = 0
+ST16 PTR
okvs_next_field_PTR_is_already_set
jsr stepptr
jmp incptr ; do NOT change this to BNE
; because if the low byte of PTR is 0
; and the value of (PTR) is also 0
; then stepptr will return with Z=1 and
; we will fall through here unexpectedly
2020-03-24 20:30:14 +00:00
incptr4 jsr incptr2
2019-09-10 13:20:32 +00:00
incptr2 jsr incptr
incptr
2019-09-24 00:01:42 +00:00
; preserves A, X, and Y
inc PTR
bne +
inc PTR+1
+ rts
2019-09-20 23:27:16 +00:00
GetStoreAddressFromAY
2020-03-24 20:30:14 +00:00
+ST16 PTR
2019-09-20 23:27:16 +00:00
jmp derefptr
2019-09-10 13:37:19 +00:00
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
2019-09-10 13:37:19 +00:00
; out: Y = 0
; preserves X
ldy #1
lda (PTR),y
pha
2019-09-10 13:37:19 +00:00
dey
lda (PTR),y
sta PTR
2019-09-10 13:37:19 +00:00
pla
sta PTR+1
rts
stepptr
; out: Y = 0
; preserves X
ldy #0
lda (PTR),y
clc
adc PTR
sta PTR
bcc +
inc PTR+1
+ rts