Jump to content

Photo

Tutorial: Arrays in Forth


10 replies to this topic

#1 Willsy OFFLINE  

Willsy

    River Patroller

  • 3,024 posts
  • Location:Uzbekistan (no, really!)

Posted Sat Oct 14, 2017 8:20 AM

Introduction

Arrays are something that you soon get to grips with in most programming languages. Just about every programming language supports them.

Except Forth.

Yes. It's a bit strange really, but Forth provides no built-in support for handling arrays of... well... anything, really.

So you can't do arrays in Forth, right? Wrong. The reason you don't get arrays "out of the box" in Forth is simply this:


"In Forth, you can implement your own arrays, and make them work just like you want them to."

 

In this article, I'm going to present my own solution, which is tiny and solves the "array problem" in a Forth-like way (i.e. making Forth do as much of the work for us, so that we don't have to).

An Array Syntax

The cool thing about Forth is that when you write Forth "code" you're just extending the language. Each new word that you write becomes part of the language, and we can use this (and indeed, you're supposed to) to our advantage. We're going to invent a little array handling lexicon (group of words) to give TurboForth support for 16-bit (1 cell) integer arrays.

Lets take a look at the typical syntax that one uses in BASIC and C for handling arrays of integers. Lets use cats as an example. We'll have a four element array that holds the number of lives for four cats: Tom, Dick, Harry, and er, Fred.

Here is how we might do this in BASIC and C:

 

Attached File  basic_c.png   3.54KB   0 downloads

 

Looks okay to me. How would we do this in Forth? Well, it's Forth, and unlike C and BASIC, we can extend the language any way we see fit. We just have to write the code and it just "becomes" part of the language.

When we declare VALUEs in Forth (a kind of variable that pushes its value to the stack when reference it) we give it an initial value via the stack, and a name, like this:

    4 VALUE CATS

So, 4 goes on the stack, the word VALUE executes, and he (VALUE) knows he has to create a new entry in the dictionary for this new value. Where does he get the name from? Well, it comes immediately after the word VALUE, so VALUE itself reads the input in front of itself and gets the name and creates it. This would work well for us with arrays: It's Forthy and Forth users would be comfortable with it immediately. In Forth, it's "common sense".
Declaring an Array

How about this to declare an array:

    size [] name

So, that's the size of the array (we're only going to talk about integer arrays), then that weird [] thing (which is what is actually going to create the array for us) and then the name of array, so that we can reference it in our code later. So:

    9 [] lives

Would declare an integer array of 9 elements, called "lives". That [] symbol is just two square brackets. It's a Forth word (that we're going to create). Remember that in Forth you can call a word anthing you like, as long as it doesn't have a space in its name. So, we'll use [] as a kind of pictogram, since array access in C, Java and Pascal is accomplished with square brackets (BASIC being the odd one, as it uses parenthesis) so [] should be obvious to us that we're dealing with arrays.
Accessing Array Elements

Okay, so we have a way to declare an array. Now we need a way to write data into the elements of the array. Again, lets look at existing Forth practice for this:

 

Writing to the Array

In Forth, when writing to variables with the word ! we first place the value to store on the stack, then the variable to store it in, then the word ! takes this information off the stack and uses it to perform the write action, like this:

    variable score
    999 score !


Since anyone that uses Forth would be comfortable with this, it makes sense to use the same idiom if we can. However, we need three items to go on the stack:

  • The value to write into the array;
  • The element number in which to write the value;
  • The name of the array itself.

Like this:

    4 5 lives

So, we want to write the value 4 into the 5th element of the lives array. But, just like ! with variables, we need a word to do the writing for us. Well, it's Forth convention to use the symbol ! to represent writing values to something, since ! has been used to write to variables since at least 1970. So, we really should pick something similar for our lexicon that follows this convention, so that it conveys meaning to the reader.

I elect (because I'm writing this article) the word/symbol [!]

    4 5 lives [!]

Here, again we're using the [] nomenclature to indicate that we're dealing with an array, but we have that rather cool ! inside the brackets, which is synonymous with ! which means write. So, the above reads as:

  • Put 4 on the stack (the value we want to write into our array)
  • Put 5 on the stack (we want to access the 5th element of our array)
  • The name of the array;
  • The word [!] which does the actual writing to the array, using the data we have placed on the stack for it.

The stack signature for [!] would be:

    value element array --

Which simply means that [!] exepcts the value, the element, and the array itself on the stack, and it removes them when it's done.

 

Reading from the Array

So, how to read from the Array? Well, when we read variables, we use the word/symbol @ (fetch). @ just needs an address on the stack, and then it uses that address, goes and reads that address and pushes whatever it finds there onto the stack. Again, we should use the same principle, as the concept is already established in Forth and we should avoid completely new paradigms and nomenclatures unless we have no choice; they're just harder to remember later on.

I elect [@] as our word to read from an array. It will need two items on the stack: The element number, and the array itself:

    5 lives [@]

So, the above would read the 5th element from the lives array, and place it on the stack. The stack signature is:

    element array -- n

So, after [@] executes, it removes the element number and the array reference from the stack, and leaves a value (n) for us - the value that it read from the array.

That's probably enough to get us started writing code. Strap in...

 

A Veritable Code Smorgasbord

Well, the first word we need to tackle is [] creates an array for us.

 

A Detour Into CREATE

There is a very useful word in Forth called CREATE which, er, creates stuff for us. Specifically, it creates words in the dictionary. We're going to go on a little detour here and discuss CREATE for while, because it does a little more than just creating a word in the dictionary for us. The words it creates are smart; they actually inherit some special built in behaviour: They push the address of themselves. Let's look at an example:

    create chaos

If you type this directly into the TurboForth command line, it will dutifully obey you, and go ahead and create that word for you. You can now execute that word, straight away, which is a little weird, since the word has no code associated with it. Nevertheless, go ahead and try it:
 

Attached File  chaos.png   1.5KB   0 downloads

 

As you can see, when we executed chaos something was pushed on the stack for us. What is that? Well, words created with CREATE reserve one cell (in TurboForth, a 16-bit (2 byte) word) of 'storage space'. In addition, words created with CREATE inherit some behaviour such that, when they are executed, they push the address of this storage space to the stack (if you are familiar with objects, you can think of words created with CREATE as objects of class CREATE, and they have a single static method, which pushes the address of its storage). In memory, it would look like this (let's assume chaos is stored at memory address B000 hex to keep things simple):

 

Attached File  memory.png   5.96KB   0 downloads

The link field is the link to the previously defined word in the dictionary and is not important to us. The length field just holds the length of the name (plus some other info, again not important to us). Then comes the 5 bytes of the name, plus a padding byte since it's an odd length. Then the Code Field. Now things are getting interesting. This is the address of some code that is executed when you execute chaos (this is where chaos' inherited behaviour comes from). So, when you execute chaos it will actually execute some code at address 63E0 (just chosen for illustration purposes - i've no idea where it goes in reality, and I wrote the TurboForth!).

What does that code do? It pushes the address of the parameter field. It's called a parameter field because we can store, well, a parameter in it. Personally I'd have called it a data field, but what do I know?

Lets prove it:

 

Attached File  chaos2.png   1.94KB   0 downloads

Here, we have created chaos, then we used ! to store 99 into it. Then we read it back.

Let's look at how this works:

 

  • Remember that chaos is "pre-defined" to push the address of its parameter/data field onto the stack when we execute it;
  • We put 99 on the stack;
  • We execute chaos, and he puts his data field address on the stack
  • We excute ! (store) which stores 99 into the address provided by chaos.

Then we read it back:

  • We execute chaos, so he pushes the address of his data field onto the stack;
  • We execute @ (fetch), so he uses the address supplied by chaos, reads that address, and puts the result on the stack;
  • We use . (dot) which takes the top item off the stack and prints it on the screen as a number. And there's the 99 that we stored.

If you're thinking "Hang on, that's just the same as a Forth variable" then congratulations. You're right. In fact the definition of VARIABLE in Forth is:

    : VARIABLE CREATE ;

The [] Word

So, now that we know that know about CREATE, we can use it (and some other tricks) to build our array defining word for us, which we have called [].

We're going to built this up as we go, illustrating concepts as we go. Little steps.

As we determined above, we need to declare an array of a specific size (which is going to be passed in on the stack), so CREATE is only going to get us so far... We still need a way to reserve some storage space. I'll just go ahead and give you the code, and then we'll break it down. You'll probably have some "aha!" moments along the way. Later on, we're going to make it more sophisticated, but first we'll keep things simple. Here's the code for our array creation word, called [] (which I pronounce out loud as (drum roll...) "create array").

    : [] ( size tib:"name" -- ) create dup , cells allot ;

Not a lot of code is it, but there's quite a lot going on here, as you're about to see. Hang in there. It's worth learning this stuff.

First, the stack signature:

    size tib:"name" --

This indicates that [] needs a size on the stack, and also needs the name of the array in the Terminal Input Buffer (tib). What on earth is the terminal input buffer? It's the buffer that you are typing stuff into on the TurboForth command line. This is just a fancy way of saying that [] requires the name of the array to follow the [] itself, like this:

    10 [] years

So, let's go through step-by-step, and see what happens when you type 10 [] years into TurboForth:

 

  • The number 10 goes onto the stack (no surprises there);
  •     The word [] executes, which:
    • Executes CREATE, which:
    • Reads ahead in the Terminal Input Buffer and identifies the string years;
    • Uses this string to create an entry in the dictionary - so we now have a new word in the dictionary called years;
  • Executes DUP. Remember that 10 that was placed on the stack? We need two of them, so we DUPd it. Now we have 10 10 on the stack;
  • Executes , (comma). Comma takes a value off the stack and compiles it (or "commas it" as we say) into the current compilation address in memory. What on earth is that? It's a pointer that allows the system to know where the next thing to be compiled should go. Have a look at the table above that shows the chaos word, with the parameter field at the end. Well, when CREATE finishes creating the word, the current compilation address (known as HERE) is pointing right at that parameter field. Now, when we execute , (comma) it takes the value 10 off the stack, and "comma's it" into memory, so it goes right into the parameter/data field. So, in our case, years has 10 sitting in its data field, which we just put there. Why did we just put that in there? Because, later on, we will need to know how big the array is so that we can do bounds checking (i.e. check that we don't do something stupid like write to the 100th element of a 10 element array). So, since we have stored the size of our array "inside" years, we'll be able to access it later. Recall that words created with CREATE push the address of their data field every time, which, in our case, will always contain the size of the array. Sorry to labour the point but it's critical to understand this.
  • Executes CELLS. This word simply takes what is on the stack (in our case, the first 10 that we typed in at the command line) and multiplies it by two. Why do we need to do that? Well, because next we're going to execute...
  • ALLOT. This is a neat word. It takes a number off the stack (20 - it was 10, but CELLS multiplied it by two) and reserves that many bytes of memory. We want to reserve cells, which are two bytes, (10 cells = 20 bytes) which is why we used CELLS convert a cell count into bytes). Internally, ALLOT "reserves" the memory by simply adding the value we just computed (20) to HERE (the current compilation address pointer). So, this is how memory would look after creating our array, years:

 

Attached File  memory2.png   7.58KB   0 downloads

As you can see, HERE is now pointing at the address just after the last byte of our array. The next word that we create in the dictionary will start "HERE". That's why it's very important that we include bounds checking. If we didn't, then the word [!] (when we get around to writing it) could possibly destroy other words in the dictionary if we supplied an out-of-range value for the element number.

Let's use DUMP to actually look at the array in memory.

First, type COLD to reset TurboForth to a known state, and let it boot. Next, enter the definition for [] as follows:

    : [] ( size tib:"name" -- ) create dup , cells allot ;

    Now, create an array, like this:

    10 [] years

Next, with the Utilities disk in DSK1, type 36 LOAD to load the dump utility. View the array in memory, like this:

    ' years 50 DUMP

The above line uses the word ' (pronounced tick) to get the address of the word 'years', we then put 50 on the stack, and call DUMP. DUMP takes the address off the stack, and the 50 (which is the number of bytes to display) and shows us the data.

 

Attached File  dump.png   7.41KB   0 downloads

See the 000A there in the second cell? That's the size of the years array, then 20 bytes of reserved space (here they have the value 20 (hex) but they could have any other value (ALLOT does not initialise the allotted space to zero)). After that, you can see the word _vect which is actually a word used by the DUMP utility. Now you can see that without bounds checking (which we haven't done yet) we could potentially write into any area of memory and destroy the dictionary.

Next, we'll add the ability to write to the array...

Writing to the Array with [!]

As discussed above in the Writing To The Array section, our [!] word will need three items on the stack. In order, these are:

  • The value to write into the array;
  • The element or index number in which to write the value;
  • And finally (at the top of the stack) the name of the array itself.

Therefore the stack signature is:

    value index array --

As we can see, there is nothing on the output side of the stack comment, meaning that [!] will consume its arguments (they'll be removed) which is standard practice. Let's roll up our sleeves and build the definition up:

    : [!] ( value index array -- ) swap cells + cell+ ! ;

So, remember when [!] executes, it expects three items on the stack, as shown in the stack comment.

The first thing it does is SWAP the index and array, such that the index is at the top of the stack. When we use [!] we'll be specifying the index in cells (16-bit words), so we execute CELLS which converts it to bytes.

Now we execute + which adds the newly converted number of bytes to the array address. The stack now looks like this:

    value array_address

Okay, pretty good. However, we have a problem... We are storing the size of the array itself in the first cell of the array space, so we need to step over it. We need to add 2 to the address to make it point to the data-space immediately after the array size.

Now, we could simply execute 2 + (or, even better, 2+) to add two to the address, and that would be perfectly fine. However, we're going to use the word cell+ which actually does the exact same thing (adds 2 to the value at the top of the stack). So why use cell+ instead of 2+? Simply because cell+, as a synonym of 2+ describes the intention of the code better. It tells the reader that we're moving to the next cell in memory, rather than adding 2 to your Space Invaders score, for example.

Okay, so we now have an address at the top of the stack, and a value underneath it, which is all that the word ! ("store") needs to do its work, so next we execute ! and the value is stored in the correct place in the array. Let's try the whole thing from scratch. Type COLD into TurboForth to reset it, and enter the following code:

    36 load
    : [] ( size "name" -- ) create dup , cells allot ;
    : [!] ( value index array -- ) swap cells + cell+ ! ;


The "36 load" loads the DUMP utility so that we can inspect memory.

Now type the following:

    10 [] years
    1949 0 years [!]
    1970 1 years [!]
    1977 2 years [!]
    1978 3 years [!]
    1980 4 years [!]
    1981 5 years [!]
    1985 6 years [!]
    ' years 50 dump

 

Attached File  dump2.png   8.28KB   0 downloads

 

Here we can see the contents of our array. Here's what we've got:

  • Note the value 10 (000A) is there (the size of our array). We haven't added bounds checking yet, we'll get [!] and [@] working first, then we'll re-visit them to include bounds checking later.
  • Next we see 079D which is 1949 in hex (the year Mark Knopfler was born),
  • then 07B2 (1970, the year this author was born),
  • then 07B9 (1977, the year Dire Straits released their seminal first album of the same name),
  • then 07BA (1978, the year of Communique, Dire Straits' second album),
  • then 07Bc (1980, the year Dire Straits released their third album, Making Movies (you maybe noticing a bit of a theme here)),
  • then 07BD (1981, the year the TI-99/4A hit the market),
  • then 07C1 (1985, the year Dire Straits released Brothers in Arms).

Yes, I do know the year of release of every Dire Straits album, and the in-order track listing of every album. I know what you're thinking.

 

Reading from the Array with [@]

Okay, so far so good. We can write to the array. Now we need to add some code to read from the array. We'll prove it works then re-vist both [!] and [@] and add bounds checking later.

Here's the definition of [@]:

    : [@] ( index array -- value ) cell+ swap cells + @ ;

Here's the break-down:

  • The first thing we do is add 1 cell (2 bytes) to the address of the array (remember, array actually leaves the address of its parameter field on the stack, which we're using to hold the size of the array).
  • Now we do a SWAP so the index is now on the top of the stack, where we can work with it.
  • Using CELLS we convert the index, which is expressed in terms of cells, into a byte value (as mentioned earlier, it just does a mulitply by two, so, for example, 5 CELLS represents 10 bytes).
  • We add the index and the array address together using +. We now have the address to read from on the stack.
  • We execute @ which reads from the address passed on the stack, and replaces it with the value read from memory.

And we're done.

That wasn't too bad was it? And, crucially, we've just defined our own way of handling arrays, which can be used in any program we write from now on.

 

Adding Bounds Checking

The above is all very well, but without bounds checking on our arrays, we run the risk of writing to areas outside of the array, with potentially disastrous results.

Consider this:

    5 [] my_array
    : test ." Hello Mother" cr ;
    $2046 14 my_array [!]
    $6174 15 my_array [!]
    test


See? We can modify code anywhere in the system. Here, we modified a string, so we got away with, but we could have written into the executable code of a word and crashed the system easily. These types of bugs can be very difficult to find, because it would look the bug occurred inside the word that got blasted over, and not in the code that did the blasting!

Adding bounds checking to our word is very simple, and, since the bounds checking will be common to both words, we'll put it in its own word, and then [!] and [@] can both use it.

We'll create a word called [?] which checks the array index against the allowed size, and will either take no action if the index is legal, or abort if it's illegal.

So the stack signature for [?] will be:

    index array -- index array

This is a little unusual, as this word does not consume or change its arguments. It's normal convention for words to consume their arguments. However, that would mean that we'd have to use 2DUP to duplicate the index and array in both [!] and [@], and I don't like that. So, I'm going to have [?] do it instead. If [?] detects an out of bounds access, it will abort with an error message and the program will stop running (and the stack will be cleared). If there is no bounds violation, the index and array address will remain on the stack un-changed.

    : [?] ( index array -- index array ) over abs over @ > abort" Array index out of bounds" ;

Since both [!] and [@] are going to use this word, it needs to be defined first. Thus, the whole program together becomes:

    : [] ( size tib:"name" -- ) create dup , cells allot ;
    : [?] ( index array -- index array ) over abs over @ > abort" Array index out of bounds" ;
    : [!] ( value index array -- ) [?] swap cells + cell+ ! ;
    : [@] ( index array -- value ) [?] cell+ swap cells + @ ;


Before we discuss how the bounds checking works, it's worth taking a moment to reflect on how much code there isn't. The above code compiles to 128 bytes. That's with bounds checking. And not only that, we implemented arrays exactly the way we wanted to, and, we can change it later if there's something we don't like about it.

 

"In Forth, you have control over everything."

Okay, lets take a look at how [?] works, as things probably look a little confusing in there:

The first thing to note is that when [?] is called, the index and array (at least) are on top of the stack. In the stack diagrams below I'll use green to indicate stack items that were on the stack before the word executes, and red to indicate stack items that the word itself puts there as it's executing. If the bounds are being checked as part of [!] then we actually have value, index, and array address on the top of the stack, but we're only interested in the top two: the index, and the array address.

We use OVER to move a duplicate of the index "over" the array address, to the top of the stack:

    index array index

Then we use ABS to check the index value. If the value is 0 or positive, the index will be un-changed. If the index is negative (for some bizarre reason) then ABS will change the index to a positive value. For example: -3 becomes 3.

Now we use over again move a duplicate of the array address "over" the index, to the top of the stack:

    index array index array

Now we execute @ which uses the array address on the top of the stack and reads the size of the array, which, as we learned above, is stored in the parameter address of the array. So our stack looks like this:

    index array index size

Next we execute > which compares the index to the size. When > executes it consumes the size and index and leaves a flag on the stack (either TRUE (-1) or FALSE (0). If the flag left on the stack is true, ABORT will abort the running program, and issue the error message. Otherwise it will take no action (apart from removing the true/false flag on the stack). Thus at the end of the word, if no index error occurs, then we are just left with the original index and array on the stack, which is used by both [!] and [@].

And there, dear Forthers, is a simple implementation of arrays with bounds checking in 128 bytes. Nothing fancy, nothing flash. Just what's needed.

 

That's Forth.


Edited by Willsy, Tue Oct 17, 2017 12:36 PM.


#2 TheBF OFFLINE  

TheBF

    Moonsweeper

  • 371 posts
  • Location:The Great White North

Posted Sat Oct 14, 2017 9:44 AM

That's a clean approach to arrays in Forth.  

 

I have to come to the conclusion that is this kind of code is clearer and faster ultimately than using the Forth CREATE DOES> construct for arrays.

 

Nicely done.



#3 Willsy OFFLINE  

Willsy

    River Patroller

  • Topic Starter
  • 3,024 posts
  • Location:Uzbekistan (no, really!)

Posted Sun Oct 15, 2017 4:09 AM

Thanks! A bit wordy/long-winded but I'm trying to give some insight into what's going on under the covers, and a bit about why we do things we the way we do!

 

It's a shame more folk aren't trying Forth - any of the incarnations!



#4 Vorticon OFFLINE  

Vorticon

    River Patroller

  • 2,808 posts
  • Location:Eagan, MN, USA

Posted Sun Oct 15, 2017 6:39 AM

You need to get that TF manual finished!!!



#5 jedimatt42 OFFLINE  

jedimatt42

    Stargunner

  • 1,260 posts
  • Location:Beaverton, OR

Posted Mon Oct 16, 2017 10:35 AM

I feel like you get away with using abs in [?] because CELLS does something you didn't describe. Such as shifting the sign bit off the end when. Is that correct?

-M@

#6 TheBF OFFLINE  

TheBF

    Moonsweeper

  • 371 posts
  • Location:The Great White North

Posted Mon Oct 16, 2017 8:28 PM

I think is more of a protection from a negative index getting into the mix.

 

Also the error detection uses '>" which is a signed operation so this implementation could only handle an array on **32K cells max.

Which of course in a stock TI-99 is big enough since the largest continuous block of memory is 24K 

 

**(because any number bigger than HEX 7FFF (32,767) is treated as negative in signed operations on the 9900)

 

The definition for CELLS on a 16 bit CPU is just:

 

: CELLS  ( n -- n )  2* ; 

 

And 2* is typically just duplicate the top of stack value  and ADD the 2 together ;

 

: 2*       ( n -- n)   DUP +  ;  \ in high level Forth. But normally coded in Assembler

 

Nothing is hidden in Forth, but it can be a little harry sifting through all the Assembly language to find the answer.


Edited by TheBF, Mon Oct 16, 2017 8:33 PM.


#7 Lee Stewart OFFLINE  

Lee Stewart

    River Patroller

  • 3,392 posts
  • Location:Silver Run, Maryland

Posted Mon Oct 16, 2017 8:43 PM

I feel like you get away with using abs in [?] because CELLS does something you didn't describe. Such as shifting the sign bit off the end when. Is that correct?

-M@

 

Nothing special about CELLS .  CELLS is a synonym for 2* as Willsy indicated.  It actually doubles the number on the stack by adding it to itself.  The array index is a cell offset, but addresses are calculated using byte offsets, hence the requirement to double the cell count (index) to get a byte count, which is what CELLS does.  Typing “ 2 CELLS ” leaves “ 4 ” on the stack.

 

Actually, I don't think ABS should be used at all.  In the arrays under discussion, a negative index is an error.  Testing the absolute value of a copy of the index may pass, but then allowing the use of the negative value cannot possibly be OK.  I think there need to be two tests, one for too large an index and one for a negative index.  The latter will catch indices so large they are negative.

 

...lee



#8 jedimatt42 OFFLINE  

jedimatt42

    Stargunner

  • 1,260 posts
  • Location:Beaverton, OR

Posted Mon Oct 16, 2017 10:45 PM

That all sounds good... Maybe Willsy wanted to support negative indexes... Or maybe because on a 64k byte addressable machine, 32k cells is 64k bytes.

I would agree that I would prefer to error on an attempt to use a negative number as an index into an array... or if you had a larger memory system, use an unsigned number.
It gets a little more significant if we decide to apply this technique to a bitset. Then you could easily wish to treat the 16 bit array index value as an unsigned number to get 64k bits.

Anyway, nit picking aside, I'm pretty sure the point was, you can build the semantics you want, for the application at hand. And I appreciate the illustration.
To date, I've worked with arrays in Forth more like you would in ASM. A heap of memory, carefully accessed with address arithmetic. But I like up-leveling like this.

I'm starting to think a domain specific language mindset and Forth go hand in hand.

Anyone every try HYPE on one of your Forths? https://web.archive....horch/hype.html

Thanks Willsy!

-M@

#9 TheBF OFFLINE  

TheBF

    Moonsweeper

  • 371 posts
  • Location:The Great White North

Posted Tue Oct 17, 2017 7:11 AM

Have not tried HYPE, but there are quite a few OOP overlays for Forth.

 

Thanks for the link.  I will give it a whirl.   And yes. Domain specific languages are how Forth is used very frequently.

Nobody knows it's Forth under the hood (bonnet) because it looks like something else. :-)

 

OOP Implementations:

Bernd Paisons did one called minioof

 

https://www.complang...ni_002dOOF.html

 

The big one was a language called NEON for the Mac.  It survives as MOPS Forth for the MAC.

It is pretty cool too.



#10 Willsy OFFLINE  

Willsy

    River Patroller

  • Topic Starter
  • 3,024 posts
  • Location:Uzbekistan (no, really!)

Posted Tue Oct 17, 2017 12:41 PM

That all sounds good... Maybe Willsy wanted to support negative indexes... Or maybe because on a 64k byte addressable machine, 32k cells is 64k bytes.

I would agree that I would prefer to error on an attempt to use a negative number as an index into an array... or if you had a larger memory system, use an unsigned number.
It gets a little more significant if we decide to apply this technique to a bitset. Then you could easily wish to treat the 16 bit array index value as an unsigned number to get 64k bits.

Anyway, nit picking aside, I'm pretty sure the point was, you can build the semantics you want, for the application at hand. And I appreciate the illustration.
To date, I've worked with arrays in Forth more like you would in ASM. A heap of memory, carefully accessed with address arithmetic. But I like up-leveling like this.

I'm starting to think a domain specific language mindset and Forth go hand in hand.

Anyone every try HYPE on one of your Forths? https://web.archive....horch/hype.html

Thanks Willsy!

-M@

 

Yes, I was just protecting from using negative indexes, though, as Lee points out, it's not a good idea really. I mean, if you calculate an index of -3, making it +3 isn't really improving the situation is it? There's clearly something wrong!

 

So it would be better to check against the size of the array (as [?] already does) and less-than-zero.

 

I'll leave that as an exercise for the reader!  :P



#11 TheBF OFFLINE  

TheBF

    Moonsweeper

  • 371 posts
  • Location:The Great White North

Posted Tue Oct 17, 2017 9:25 PM

This kind of thing is commonly solved using the U< operator. ("un-signed less than"  for those not familiar with Forth)

 

If the array size is 1000 for example, and I ask for index 500

 

500 1000 U< returns TRUE  so it's a good index.

 

BUT... if I ask for  index 1001

 

1001  1000 U<  gives me FALSE (1001 is NOT less-than 1000)   so I should abort with an error.

 

And if I  ask for index -1 or -2  those numbers are actually 65,535 and 65,534 unsigned.

 

-1    1000 U<   returns FALSE because it is actually    65535  1000 U<.

 

So U< lets you protect against negative values and "too high" positive values with one comparison.

 

Neat trick of twos complement arithmetic.  

 

https://en.wikipedia...wo's_complement






0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users