Forth Lesson 6
Review
In the previous lesson, we learned how to:
- Examine words with "see" and "(see)"
- Disassemble machine code with "dis"
- Find words with "sifting" and "sift-devs"
- Find uses of a word with ".calls"
- Find execution tokens with "'" and "[']"
Data
There are several ways to store and retrieve data values.
Variables
ok variable myvar
"variable" defines a new word (parsing the new word name from the input stream, as with ":") and allocates enough data space for a number (e.g. 32-bits). When you later execute the new word ("myvar" in this case), it pushes the address of that data area. The stack diagram for the new word is "( -- adr )". The initial contents of the data is undefined.
You can put data into the variable with:
ok 12345 myvar !
"!" is pronounced "store". Its stack diagram is "( n adr -- )".
To get the data back, use
ok myvar @ . 12345 ok 555 myvar ! ok myvar @ . 555
"@" is pronounced "fetch". Its stack diagram is "( adr -- n )". Note that, after the example above, the data value is on top of the stack. To display it, you would need to say "." afterwards (unless "showstack" mode is on).
When you decompile a variable (e.g. "see myvar"), the decompiler may identify it as a "user" word. That is an implementation detail that is related to the fact that many Forth systems support multi-threading. A "user" variable is one that is allocated in the per-thread data area.
Other Memory Access Operators
The size of the number that "variable", "@", and "!" use is the "natural" size for the implementation, similar to "int" in C.
"@" and "!" are not limited to addresses returned by variables; they can be used with arbitrary memory addresses. However, for externally-defined data, it is usually a good idea to use memory access operators that explicitly specify the data size.
l@ ( adr -- l ) \ Fetch a 32-bit value l! ( l adr -- ) \ Store a 32-bit value w@ ( adr -- w ) \ Fetch a 16-bit value <w@ ( adr -- w ) \ Fetch a 16-bit value, sign extending w! ( w adr -- ) \ Store a 16-bit value c@ ( adr -- b ) \ Fetch a 8-bit value c! ( b adr -- ) \ Store a 8-bit value
Don't use bare "@" and "!" for accessing memory-mapped I/O registers, because some implementations may perform "@" and "!" with multiple machine instructions in order to handle unaligned addresses. The explicit-sized operators listed above will usually work, but for ultimate portability when writing FCode device drivers, there are some other I/O access operators that make strong guarantees (byte ordering, atomicity, non-cached, etc). That is a topic for later.
Values
"value" is an alternative to "variable" that is more convenient to use in many situations:
ok 4567 value myval ok myval . 4567 ok 98 to myval ok myval . 98
As usual with "defining words" (words that create other words, like ":" and "variable"), "value" parses the name of the new word from the input stream.
Unlike variables, values
- Are explicitly initialized when created (the inital value comes from the stack)
- Are "self fetching", i.e. the new word pushes the value instead of the address that contains the value.
- Must be preceded by "to" in order to set the value.
n.b. is and to are synonyms in OFW. The first inherits from F83, the second from ANS Forth. "to" is not specific to values; it can also set the data for some other kinds of words we haven't learned yet. "to" is fairly "smart", and thus is a little slower than "!" (whose implementation is trivial). The speed difference is irrelevant in most cases. As compensation for "to myval" being a bit slower than "myvar !", "myval" is a bit faster, and slightly more space-efficient, than "myvar @". So for the common case where you retrieve the data more often than you set it, values are a good choice. Furthermore, code that uses values is somewhat easier for humans to read.
Longer Data Structures
Here is how to create a table with multiple data values:
ok hex ok create my-table 000 , 111 , 222 , aaa , bbb , fff , ok my-table @ . 0 ok my-table 4 + @ . 111 ok my-table 2 na+ @ . 222 ok my-table 4 na+ @ . bbb
"create" is a defining word that makes a new word ("my-table" in this case) and marks the beginning of a data area that will be allocated by later words like ",". Later execution of the new word (e.g. "my-table") pushes the beginning address of that data area, so you can use memory access operators like "@" and "!' to read or write it. The stack diagram of the new word "my-table" is "( -- adr )", the same as a variable.
Here are some operators that are useful after "create":
, ( n -- ) \ Allocate data space for a number and put n there c, ( b -- ) \ Allocate data space for a byte and put b there w, ( w -- ) \ Allocate data space for 16 bits and put w there l, ( w -- ) \ Allocate data space for 32 bits and put l there allot ( n -- ) \ Allocate data space for n bytes (uninitialized)
The data space that is allocated by consecutive calls to these words is contiguous, so you can do arithmetic on addresses to get from one item to the next. However, that guarantee only lasts until you define the next word.
In the "my-table 4 + @" example above, we assumed that numbers are 32-bits, using "4 +" to get from the table base address (as returned by "my-table") to the address of the second number. That's fine for interactive use on a known system, but for code you want to save, it's better to write something more portable, hence the use of "na+" in later lines. There are several such address arithmetic operators:
na+ ( adr n -- adr' ) \ Add n times the size of a number to adr ca+ ( adr n -- adr' ) \ Add n times the size of a byte to adr wa+ ( adr n -- adr' ) \ Add n times the size of a 16-bit word to adr la+ ( adr n -- adr' ) \ Add n times the size of a 32-bit longword to adr
On byte-addressed machines (nearly all machines these days), "ca+" is equivalent to just "+", so it is rarely used. "la+" is equivalent to "4 * +" (multiply n by 4 and add the result to adr), but is faster, more space-efficient, and more explicit, so "la+" is highly recommended.
You can make arbitrary data structures after "create"; they are not limited to arrays of the same basic number size. To access the items, you can either do explicit address arithmetic on the table base address or define some named helper words to add the offsets for specific items. There is a "field" defining word to make that easier, a topic for later.
Constants
Suppose you need a Forth word that returns (i.e. pushes on the stack) a single numeric value. You could do that with a colon definition, as in:
ok : mynum 12345 ;
However, constants are important enough that Forth has a special defining word for them:
ok 12345 constant mynum
Functionally, the two different definitions of "mynum" are equivalent, but the "constant" definition will execute slightly faster and occupy less space. (If you subsequently call "mynum" within another definition, the space occupied by the call is usually the same for either form; it is just the definition of "mynum" itself that is more compact in the "constant" case.)
When you are writing code in a file for saving, it is a good idea to specify the number base explicitly, instead of assuming a particular value (e.g. hex or decimal) for the current number base. So in a file I would write:
d# 12345 constant mynum
or
h# 1a234f constant mynum
The only case where I intentionally break this rule is in a large table of numbers or in a large definition that is completely dominated by numbers (such as a list of register values). In those cases, I sometimes explicitly set the base at the top and list the numbers without prefixes, so the proliferation of prefixes doesn't obscure the code.
Efficiency of Constants
Using a named constant in a colon definition is marginally faster than using a literal number, and the colon definition is smaller. Specifically, comparing
ok 1234 constant foo ok : add-foo foo + ;
to
ok : add-foo 1234 + ;
The former definition of "add-foo" takes a little less space in the system, and executes marginally faster, than the latter definition. However, the space savings inside "add-foo" are more than made up for by the space used by the definition of the constant "foo". The break-even point for the space of a constant definition is about three later uses inside a colon definition.
At some level this is a trivial detail, but it introduces an interesting point. In Open Firmware, the numbers 0 through 8 are actually defined as constants, thus saving a useful amount of space in the system image compared to the size if they were handled as literals in the ordinary way.
ok see 5 5 constant 5 ok see 1234 1234 ?
Stylistic Point - Too Many Constants
Soapbox alert
Here I am going to espouse an heretical viewpoint. It is considered good programming practice (practically drilled in at gunpoint) to give symbolic names to all constant values. The constants are usually defined in a header file, then the symbolic names are used in later code. There are a few cases where I disagree with that practice.
The most compelling case is hardware registers with numerous bitfields. Many common hardware devices have tens or even hundreds of registers, each with several sub-fields. You might think that the accepted "define symbolic names for everything" practice would make code easier to read, but I generally find it harder to read, sometimes much harder. The reason is because the symbolic names are rarely sufficiently descriptive to understand the details without also consulting the hardware data sheet. To really understand the code, or to debug by looking at actual numbers in registers, you have to simultaneously correlate several things - the source code file, the header file, and the data sheet. It helps when the symbolic names in the code exactly match the data sheet names, but they often don't, so you usually end up having to do the correlation by the register numbers and bit numbers. It's even worse when the header file is hierarchical, with the base offsets to register blocks defined separately from the offsets to individual registers. I understand the "abstraction" arguments for expressing things this way, but the fact is that specific hardware devices are not really abstract, and their numbers (register addresses and bitfields) almost never change after the device goes into production.
This being the case, in my device driver code, I often hardcode specific numbers inside definitions, rather than first defining constants and then using them. For documentation, I just include a comment at the end of the line telling the name of the register and the meaning of the bits. That way all the relevant information is on the same line - you see the numerical values, what they mean, and the context in which they are used all in one place.
Furthermore, in many cases, the majority of registers are touched exactly once in the driver, so finding everything related to a given register is very easy. For a register that needs to be accessed several places, it is often possible to identify a common code subsequence around those accesses. In that case you can encapsulate that sequence as a Forth word, thus reducing the number of direct mentions of the register to one. This usually makes the code smaller and more reliable.
This diatribe applies only to constants that "can't" change. Defining constants for things that could very easily change, especially if they are used in a lot of places and must be kept consistent, is a very good thing.
Thus endeth the lesson.