Dynamic list in FORTH

source

What‽

Well, I started a collection of "useful" FORTH words to be used once I need them. It also seems like a nice exercise / passtime activity.

When searching for some FORTH implementation with a specific feature (I will probably write about this later) I stumbled across a specific Hacker News post which I, as one could expect, forgot to bookmark. The post itself is not really important, but someone in the replies complained about FORTH not having resizable arrays. This statement is true in fact, as FORTH does not have any type of arrays. It does however have ALLOCATE and RESIZE, which are sufficent enough.

It made me feel like writing a more complex dynamic list system tho, so here it is I guess...

Implementing basics

I started by making a simple 'list%' structure.

begin-structure list%
  field: list-len
  field: list-ptr
end-structure

: new-list64 ( n -- a ) \ len
  \ list
  list% allocate throw
 
  \ contents
  2dup list-len !
  swap cells allocate throw
  over list-ptr !
;

The words have 'list64' because one cell is 64 bits in most modern FORTHs (at least in those that I used). It should still work on non-64-bit-cell FORTHs. I specified size, as user might want to add more types of lists later.

'list%' itself is quite universal tho, so it is still just 'list%'.

I'm not sure what the correct naming of constructors is, but 'new-<STRUCT_NAME>' seems fine by me.

Then I thought that I might want to inicialise the new list with some values right away, so I made 'new-list64-fill':

: new-list64-fill ( n* -- a ) \ list of values ended by length
  dup new-list64              \ example: 14 6 42 3 new-list64-fill

  swap
  dup 0<> if
    0 swap 1- do \ start from the end, so that the order is preserved
      \ n* list-ptr
      swap
      over list-ptr @ i cells + !
    -1 +loop
  else
    drop
  then
;

Again, I have no idea what the correct way for this type of argument is, and I was (and still am) too lazy to search the archives so I improvised.

You call it with a bunch of valuse on the stack ended by their length. The order is preserved so that is nice.

Now the main part, resizing:

: list64-resize { n a -- } \ delta list64
  a list-ptr dup @
  a list-len dup @ n + 
  \ ptr-adr ptr len-adr new-len

  dup rot !
  cells resize throw
   
  swap !
;

First of all yes, I use local variables. I felt like it was a bit too much juggling with values so I made it a bit more readable. I believe that you should use the best tool for the job, and sometimes local variables are the right tool, even in a concatenative languages. Maybe someone more skilled would be able to write it in a readable manner even without locals, but not me.

Next, it takes the difference in element count. If you try to remove more elements than there are, it will error out. I think that errorring out is a resonable responce, as it is what RESIZE itself does after all.

Remember that ALLOCATE and RESIZE take number of bytes. I forgot and wondered why it doesn't copy.

Implementing fluff

So I have a dynamic list, right.

YES!

But I want more features, so here we go:

Remove item by index

: list64-remove { n a -- } \ index (starting at 0!) list64
  \ if remove the last, use normal resize
  n a list-len 1- = if
    -1 a list64-resize
    exit
  then

  a list-ptr @ n cells + \ position of remove
  a list-len @ n 1- - \ number of items left

  \ shift all values comming after
  0 do
    dup i 1+ cells + @
    over i cells + !
  loop
  drop
  \ resize anyways
  \ probably could be all written better,
  \ but I want it to be single and readable word
  -1 a list64-resize
;

I just shift all items after the removed one one spot back and remove the last item with 'list64-resize'. Standard procedures.

And yes, I should learn dictionaries next...

Append

: list64-append ( n a -- )
  1 over list64-resize
  dup list-len @ 1- cells
  swap list-ptr @ + ! 
;

Just resize and write a value to the last position.

Insert by index

: list64-insert { v n a -- } \ value index (still 0 based) list64

  \ give to regular append if inserting at the end
  n a list-len @ = if
    v a list64-append
    exit
  then

  1 a list64-resize
  a list-ptr @ n cells + \ position of append
  a list-len @ n - 1- \ number of items after insertion

  \ shift everything one further
  1 swap do
    dup i 1- cells + @
    over i cells + !
  -1 +loop

  v swap !
;

Very similar to 'list64-remove'. Now I just add space and start shifting from the end. Then just write in the value.

Pop and shift

\ remove last and return it's value
: list64-pop ( a -- n )
  dup list-ptr @
  over list-len @ 1- cells + @
  -1 rot list64-resize
;

\ remove first and return it's value
: list64-shift ( a -- n )
  dup list-ptr @ @
  0 rot list64-remove
;

If you want to remove first or last item. Will also return the value removed.

How do you end this again?