Forth First Aid


  1. Where can I find more information about Forth programming language?
  2. Where can I use pure Forth in crontabs? Can you give more details?
  3. How can I declare a variable or define a word outside of a particular task, e.g. in the very beginning of a crontab file?
  4. Which string types are used in SP-Forth?
  5. What means does Forth provide to check if two strings are identical?
  6. What other "advanced" ways of working with strings are there?
  7. How to convert a number to a string and vice versa?
  8. What means does Forth provide for creating a simple loop?
  9. How can I "insert" a numerical variable into a text string so that I could use it, for example, in MSG: or in SEND-KEYS:?
  10. And what about a double number? Is it possible to "insert" it into a string, too?
  11. Did I get it right that within a string (between quotation marks) one can place a pair of percent signs ("%%") and insert any sequence of Forth words between them?
  12. How can I see the list of all Forth words in nnCron's dictionary?
  13. Hmmmm... And how can I find the word I need (I only can recall a part of it)?
  14. How can I use value of a variable (or current value on the stack) as a parameter of some word?
  15. How to write data to a file or read from one?
  16. How can I handle lists?

Where can I find more information about Forth programming language?

Here are some useful links for beginners:

Don't forget to check onine versions of Starting Forth, a great book by Leo Brodie - http://home.iae.nl/users/mhx/sf.html and Thinking Forth by Leo Brodie - http://thinking-forth.sourceforge.net/ . There also exists a Stephen Pelc book Programming Forth - http://www.mpeltd.demon.co.uk/books.htm .

There is a newsgroup, COMP.LANG.FORTH, which is dedicated to Forth language.


Where can I use pure Forth in crontabs? Can you give more details?

"Pure" Forth can be used immediately after word "#( task-name". And word ")#" returns nnCron to classic mode. Therefore, you can define any Forth words between "#(" and ")#"

Example:

#( task1
: x1 .... ;
: x2 .... ;
: x3 .... ;
Action:
    x1 x2 x3
)#

#( task2
Action:
    x3 x2 x1
)#

How can I declare a variable or define a word outside of a particular task, e.g. in the very beginning of a crontab file?

Outside of "#(" and ")#", you can use any Forth words or expression by using them within "<% ... %>" construct. This type of construct can be located in any place of a crontab, including its beginning.

Example:

<%
VARIABLE x
VARIABLE y
: z .... x @ .... y ! ;
%>

#( task1
Action: z
)#

Which string types are used in SP-Forth?

1) Character string (au-string):

The strings of this type are most often used strings in SP-Forth. Such a string is specified by a cell pair ( a u -- ) representing its starting address and length in characters. Use a S" xxx" syntax to create character string. For example, after you enter in console

S" my first string" 

and press <ENTER>, two values will be placed on the stack - the address of the first character of the newly created string and the number of its characters.

Most of nnCron words, that are working with strings are expecting for character string as their argument. For example, you can use the word TYPE ( a u -- ) to print character string to the console:

S" my first string" TYPE

Pay attention to the fact, that the length of a strings created using S" xxx" syntax can not exceeds 255 characters.

2) Zero-terminated string (az-string):

These strings are used when calling API functions and when the string length can exceed 255 characters: the length of zero-terminated strings is not limited.

Use Z" xxx" or S" xxx" DROP syntax to create zero-terminated string:

Z" null-terminated string"
S" null-terminated string" DROP

Special words are available to convert character string to zero-terminated string and vice-versa:

#( test_count
NoActive
Action:
    \ converting character string to a zero-terminated string:
    S" test string" S>ZALLOC
    \ separating string address to an address and length in characters:
    ASCIIZ>
    \ displaying the string in a message:
    MsgBox
)#

3) Counted string (ac-string):

Counted string is a sequence of bytes, that represents the string contents. The first byte in this sequence holds the string length in characters (binary
representation of the number of data characters). This byte is called length character. Counted string in memory is identified by the address of its length character. This string type was widely used in early Forth implementations. Currently there are not too many words, that are expecting for counted strings as their arguments. The length of counted strings can not exceeds 255 characters.

Use C" xxx" syntax to create counted string. In addition, counted string can be created by using the word PLACE

C" counter string"
S" your string" buffer_name PLACE

You can convert counted string to a character string using the word COUNT ( a -- a u ), that separates counted string address to an address and length in characters.

#( test_count1
NoActive
Action:
    \ creating counted string:
    " test string"
    \ converting counted string to a character string
    \ and printing it to console:
    COUNT TYPE CR
)#

#( test_count2
NoActive
CREATE my_strbuffer 256 ALLOT
Action:
    \ creating counted string:
    S" test string" my_strbuffer PLACE
    \ printing buffer address to console:
    my_strbuffer . CR
    \ separating address to an address and length in characters
    \ and printing character string to console:
    my_strbuffer COUNT TYPE CR
    \ displaying character string in a message:
    MSG: "%my_strbuffer COUNT%" 
)#

What means does Forth provide to check if two strings are identical?

You can use word COMPARE which returns 0 if compared objects are identical.

Example:

S" first string" S" second string" COMPARE 0=
IF MSG: "Strings are the same"
ELSE MSG: "Strings are different"
THEN 

What other "advanced" ways of working with strings are there?

This is a very difficult question to answer, for there are so many possible answers. To make the answer as precise as possible, let me use you some examples.

Example 1:

#( forth_strings
\ creating 2 strings up to  255 characters each 
\ with static memory allocation
CREATE str1 256 ALLOT
CREATE str2 256 ALLOT
Action:
    \ Word PLACE puts a string into specified location
    S" This is a first string" str1 PLACE
    \ Displaying a message with the first string. Using word COUNT  
    \  to convert  an address into address with string length.
    MSG: "%str1 COUNT%"
    \ +PLACE adds another string to the specified string
    S" , and this is an addition to it..." str1 +PLACE
    MSG: "%str1 COUNT%"
    \ removing 5 leftmost characters from str1 
    \ and placing the result to str2
    str1 COUNT 5 /STRING str2 PLACE
    MSG: "%str2 COUNT%"
    \ removing 5 rightmost characters from str1 
    \ and placing the result to str2
    str1 COUNT 5 - 0 MAX str2 PLACE
    MSG: "%str2 COUNT%"
    \ placing the first 10 characters from str1 to str2
    str1 COUNT 10 MIN str2 PLACE
    MSG: "%str2 COUNT%"
    \ placing to str2 the last 10 characters from str1
    str1 COUNT DUP 10 - 0 MAX /STRING str2 PLACE
    MSG: "%str2 COUNT%"
    \ placing 10 characters from  str1 to str2,
    \ starting with the 11-th character
    str1 COUNT 10 /STRING 10 MIN str2 PLACE
    MSG: "%str2 COUNT%"
)#

Example 2:

#( forth_strings1
\ creating 4 strings up to  255 characters each 
\ with static memory allocation
CREATE str_1 256 ALLOT
CREATE str_2 256 ALLOT
CREATE str_3 256 ALLOT
CREATE str_4 256 ALLOT
\ A new word,  StringReplace str2 str1 str3 str4,
\ Searches  str_1 for substrings matching  str_3, 
\ and if it finds anything, it replaces all found matches by 
\  str_4. The result is placed to str_2.
\ If no matches are found, it places 
\ the entire str_1 to str_2, without any modifications
\ The example is not uneversal in that it was written to process 
\ a particular phrase.
: StringReplace ( -- )
  str_2 0!
  str_1 COUNT
  BEGIN OVER SWAP str_3 COUNT SEARCH WHILE
  >R SWAP 2DUP - str_2 +PLACE
  str_4 COUNT str_2 +PLACE
  R> str_3 C@ /STRING
  REPEAT
  str_2 +PLACE
  DROP
;
\ Another new word: StringReplace str str str str
\ It can function in the same way   
\ that the preceding one, but it is universal 
\ because it uses local variables, i.e..
\ it doesn't have to use  str_1, str_2  etc.
: StringReplace2 { a2 a1 u1 a3 u3 a4 u4 \ rest -- a2 u2 }
  a2 0! 
  a1 u1 
  BEGIN OVER SWAP a3 u3 SEARCH WHILE 
  TO rest SWAP 2DUP - a2 +PLACE 
  a4 u4 a2 +PLACE 
  rest u3 /STRING 
  REPEAT 
  a2 +PLACE 
  DROP  
;
Action:
    \ placing a long phrase into str_1
    S" This string is not a very good sample. " str_1 PLACE
    S" But not just a sample." str_1 +PLACE
    \ defining a search phrase
    S" not" str_3 PLACE
    \ defining a replacement phrase
    S" " str_4 PLACE
    StringReplace str_2
    MSG: "%str_2 COUNT%"
    str_2 S" test test test" S" es" S" ouris" StringReplace2
    MSG: "%str_2 COUNT%"
)#

Example 3:

#( forth_strings2
VARIABLE position
\ New word. It is used to search a string for matches 
\ with another string and (if any found) to return the position
\  of the first character of this  match.
\ Position numbering starts with 1. If the substring is not found, 
\ it returns 0. Of course, it is also possible to start the position count from 0
\ and to return -1 if nothing is found
: StringGetPos { a1 u1 a2 u2 -- pos}
a1 u1 a2 u2 SEARCH IF DROP a1 - 1+ ELSE 2DROP 0 THEN ;
Action:
    \ comparing the strings and writing the result 
    \ into variable position
    S" 123456789" S" 567" StringGetPos position !
    \ displaying the result (should be 5)
    MSG: "Position: %position @%"
)#

There are many other words that are used to work with strings. For example:

\ returns 0 if the strings are identical
S" str1" S" str2" COMPARE
\ returns 0 if the strings are identical (not case-sensitive)
S" str1" S" Str2" ICOMPARE
\ returns TRUE if a match is found
S" str1" S" ?tr*" WC-COMPARE
\ the same as SEARCH, but not case-sensitive
S" xxxstryyu" S" STR" ISEARCH

How to convert a number to a string and vice versa?

Number to string:

nnCron has word N>S. Here is its definition:

: N>S ( u -- addr u) DUP >R ABS S>D <# #S R> SIGN #> ;

It is used in this way:

100 N>S \ produces string 100

Similar results can be obtained with the help of construct S>D <# #S #>, and in this case one can specify how many characters should be converted, add necessary characters to the string, and convert characters with a sign:

101 S>D <# #S #> \ produces string 101
101 S>D <# # # # # # #> \ produces string 00101
-101 DUP ABS S>D <# #S ROT SIGN #> \ produces string -101
101 S>D <# # 58 HOLD #S #> \ produces string 10:1

Notes:

To get a string representation of a number in hexadecimal form, word N>H:

200 N>H \ produces string  C8

String to number:

For this purpose, use S>NUM. It is used in this way:

S" 128" S>NUM \ produces number 128

Example:

#( test_int_to_string
\ picking  IP addresses one by one, in order,  starting with 128.128.128.1
\ and ending at 128.128.128.10
NoActive
CREATE STR 256 ALLOT
VARIABLE NUMBER
Action:
    BEGIN
    S" 128.128.128." STR PLACE
    NUMBER @ 1 + NUMBER !
    NUMBER @ N>S STR +PLACE
    TMSG: "%STR COUNT%" 1
    \ instead of just displaying a resulting  IP address, 
    \ you could, for example, "feed" it to HOST-EXIST: 
    NUMBER @ 10 =
    UNTIL
)#

What means does Forth provide for creating a simple loop?

If you know in advance how many times the loop should be repeated, then best way to do it would be to use DO ... LOOP construct (definite loop):

10 0 DO <body of the loop> LOOP

Here 10 is the limit and 0 is the index (a number which is incremented by 1 with each iteration). To access the index from the body of the loop, use word "I" (please note that this is an uppercase letter "i", not a slash or vertical bar)—it puts the current value of the index into data stack .

The loop will be executed over and over until the index becomes equal to the limit. Therefore, the above loop will be executed 10 times.

Example:

Action:
    3 0 DO
        \  message  will be displayed 3 times
        TMSG: "Inside the loop" 1 
        \ numbering starts with 0, index equals 1
        I 1 = 
        \ displaying another message
        IF TMSG: "Second loop" 1 THEN 
    LOOP

To cause the index to be incremented by any other number then 1, use word +LOOP:

50 0 DO <body of the loop> 5 +LOOP \ incrementing by 5

To force an exit from a definite loop, use word LEAVE:

Action:
5 0 DO
    \ message will be displayed four times
    TMSG: "Inside the loop" 1 
    \ exiting the loop after the fourth iteration
    I 3 = IF LEAVE THEN 
LOOP

If the number of iterations is not known, you can use the BEGIN ... UNTIL construct (indefinite loop, which is repeated until a specified condition is met). It is used in the following way:

BEGIN <body of the loop> <condition> UNTIL

Such a loop will be executed at least one time and then its iterations will go on until a certain condition becomes true:

Action:
    BEGIN 
        \ repeating this message
        TMSG: "Warning! xxx.txt not exist!" 3 
        \ until the following file appears
        FILE-EXIST: "c:\xxx.txt" 
    UNTIL

Here is another type of a indefinite loop (or loop with a condition):

BEGIN <condition> WHILE <body of the loop> REPEAT

The loop is repeated until the condition is true.

And here is an example showing you how you can display a value of index (I) without creating an additional variable:

3 0 DO
    I S>D <# 0 HOLD #S S" I=" HOLDS #> 1 TimeMsgBox
LOOP

How can I "insert" a numerical variable into a text string so that I could use it, for example, in MSG: or in SEND-KEYS:?

Here's how:

%VARIABLE_NAME @%

Now this variable's value can be displayed in a message (MSG, TMSG) and/or printed with SEND-KEYS etc.

Example:

VARIABLE MyVariable \ creating a variable
Action:
    13 MyVariable ! \ setting the value of  MyVariable  to 13
    MSG: "MyVariable = %MyVariable @%"
    START-APP: "notepad"
    PAUSE: 1000
    BEGIN \ starting a loop
        SEND-KEYS: "%MyVariable @% {DELAY 300}"
        MyVariable @ 1 - MyVariable ! 
        MyVariable @ 0 =
    UNTIL \ ending  the loop
    SEND-KEYS: "{ENTER}That's the end, folks!"

Please keep in mind that you can also insert into a text string any Forth word which return a string ( -- addr u) or a 32-bit integer ( -- n). In order to do that, you just have to surround the word by percent signs.

Example:

\ the number of milliseconds since system startup, 
\ is inserted into a string
MSG: "%GetTickCount%"

And what about a double number? Is it possible to "insert" it into a string, too?

Sure. Here is an example of this:

#( test_dir_size
NoActive
2VARIABLE size
Action:
    RECURSIVE DIR-SIZE: "c:\cpp" size 2!
    MSG: "c:\cpp size = %size 2@ <# #S #>%"
)#

Handling double numbers is discussed here.


Did I get it right that within a string (between quotation marks) one can place a pair of percent signs ("%%") and insert any sequence of Forth words between them?

Yes, you got it right. "%...%" construct is dynamically processed, that is to say, when the prefix string is parsed, the sequence between "% % " signs is analyzed separately and word EVALUATE is applied to this substring. Please note that if you use postfix notation you will have to use word EVAL-SUBST in order to "unfold" contents of "%...%" construct.

Example:

\ creating a buffer
CREATE buf_ex 256 ALLOT
Action:
\ placing a string to this buffer
S" example" buf_ex PLACE
\ printing the string to console including the contents of  buf_ex
S" string containing %buf_ex COUNT% buffer" EVAL-SUBST TYPE CR
\ displaying a message with the same string
MSG: "string containing %buf_ex COUNT% buffer"

To your information: if a Forth word or a variable returns a string, you can skip the %...% altogether when using postfix notation:

\ "prefix" notation:
MSG: "%USERNAME%"
\ postfix notation:
USERNAME MsgBox
\ of course, you can do it this way too  (but less efficiently):
S" %USERNAME%" EVAL-SUBST MsgBox

How can I see the list of all Forth words in nnCron's dictionary?

That is very simple: open FORTH console, enter command WORDS and hit <ENTER>. But be prepared to see a list of about 3000 words! :)

And the following task will send the entire dictionary to nncron.out so that you can study it later:

#( words-task
NoActive
Action:
    WORDS
)#

(The console must be closed).


Hmmmm... And how can I find the word I need (I only can recall a part of it)?

It is just for such an occasion that nnCron's developer has defined word WORDS-LIKE in plugin tools.spf.


How can I use value of a variable (or current value on the stack) as a parameter of some word?

Since nnCron 1.89rc2 there are special words to use numbers and strings from the stack as a parameters:%n esPICK% and %n esPICKS%.

Besides that, each prefix-type word (:) has a corresponding postfix word (either , or some other name). And it is these postfix words that should be used when the current value on the stack is used as an argument.

Examples:

BEEP: 1000 500 -> var_name @ var_name1 @ BEEP
WIN-SHOW: "xxx" -> S" xxx" WIN-SHOW
WIN-HIDE: "xxx" -> S" xxx" WIN-HIDE 
\ ...
FILE-APPEND: "file" "xxx" -> S" xxx" S" file" FAPPEND
FILE-WRITE: "file" "xxx" -> S" xxx" S" file" FWRITE 
FILE-COPY: "file1" "file2" -> S" file1" S" file2" FCOPY

Prefix words of ":" type were introduced expressly in order to save users the trouble of puzzling out how to use the reverse Polish notation.


How to write data to a file or read from one?

You can easily write text data to a file using features provided by nnCron. Read the Help sections discussing such words as LOG:, FILE-WRITE:, FILE-APPEND:...

As to reading an entire text file, the simplest way to do this is as follows:

S" file-name" FILE

or even

FILE: filename

The first method is convenient when you have to get the file's name form a variable, and the second one is for use between percent signs (% %), so it takes as its parameter (file name) the sequence of characters to the end of the line.

\ first method
: 2nd-file S" %JT%\test.txt" EVAL-SUBST FILE ;
Action:
FILE-APPEND: "%JT%\new.txt" "%2nd-file%"
\ second method

MSG: "%FILE: test.txt%"

Example:

#( test_read_from_file
NoActive
\ reading 64 characters from file  test.txt  
\ and placing them to array  file_contents. 
\ displaying a message with the content of array.
CREATE file_contents 256 ALLOT
Action:
    PAD 64 S" test.txt" FREAD file_contents PLACE
    MSG: "%file_contents COUNT%"
)#

To read information from a file line by line, we'll need to use SP-Forth word READ-LINE. Here is its description:

READ-LINE ( c-addr u1 fileid -- u2 flag ior ): read the next line from the file specified in fileid into memory address c-addr. No more then u1 characters will be read. String buffer c-addr must have length of minimum u1+2 characters (including end of line characters). If the action is successful, flag will be "true" and ior will equal zero. If end of line is reached before u1 characters has been read, then u2 is a number of actually read characters without "end of line" characters. If u1=u2, end of line has been reached already.

#( test_read_by_line
NoActive
\ Reading  file test.txt line by line (in a loop) 
\ into array  list-contents
\ and displaying each line in a message box.
\ Lines in file should not be longer then 255 characters.
VARIABLE list-file
CREATE list-contents 258 ALLOT
Action:
    S" test.txt" R/O OPEN-FILE-SHARED THROW list-file !
    \ writing the string into the second postion of array list-content
    BEGIN list-contents 1+ 255 list-file @ READ-LINE THROW WHILE
    \ Number of read characters remains on the stack..
    \ We will save it in the first position of array
    \ in order to get a string with a character count
    list-contents C!
    MSG: "%list-contents COUNT%"
    REPEAT
    DROP
    list-file @ CLOSE-FILE DROP
)#

#( test_read_by_line1
NoActive
\ Reading  file test.txt line by line (in a loop) 
\ into variable list-contents
\ and displaying each line in a message box.
USER-VALUE list-file
USER-VALUE list-contents
\ maximum length of line  (number of characters)
10240 CONSTANT max-line-size
Action:
    \ Allocating sufficient space in memory
    \ and recording its address into list-contents    
    max-line-size ALLOCATE THROW TO list-contents
    S" test.txt" R/O OPEN-FILE-SHARED THROW TO list-file
    BEGIN list-contents max-line-size 2- list-file READ-LINE THROW WHILE
    \ The number or read characters remains on the stack.
    \ Adding it to address of allocated memory
    \ and adding 0 to get a zero-terminated string
    list-contents + 0 SWAP C!
    MSG: "%list-contents ASCIIZ>%"
    REPEAT
    DROP
    list-file CLOSE-FILE DROP
    list-contents FREE DROP
)#

How can I handle lists?

A list is a special type of data structure where each element (node) contains a reference to the next element in the list which unites the data and simplifies navigation of the list. Such a list is often called "linked list". The last element of a list does not contain a reference and returns a zero if you attempt to get the address of the next element. This permits us to easily learn if the list is empty or not. To declare and name a list use word VARIABLE.

Here are the words for working with lists:

To illustrate this, let's create an elementary list, add some nodes to it, print their values to console, purge the list and make sure that it is really empty:

#( test_list
NoActive 
\ creating a list
VARIABLE MY_LIST
\ defining a word that will print 
\ contents of a node to console
: show_values NodeValue . CR ;
Action:
    \ adding nodes and their values to the list
    1 MY_LIST AppendNode
    2 MY_LIST AppendNode
    3 MY_LIST AppendNode
    \ printing to console addresses or the first,
    \ second and third nodes
    MY_LIST @ . CR
MY_LIST @ @ . CR
MY_LIST @ @ @ . CR \ now printing to console \ the values stored in the list nodes ['] show_values MY_LIST DoList \ printing the same values "manually" MY_LIST @ NodeValue . CR MY_LIST @ @ NodeValue . CR MY_LIST @ @ @ NodeValue . CR \ purging the list MY_LIST FreeList \ making sure that the list is empty: MY_LIST @ IF S" List is not empty" TYPE CR ELSE S" List is empty" TYPE CR THEN )#

Word ['] compiles the address of the next word in the definition as a literal, i.e. instead of the actual word we substitute the address where its definition is stored.

Another "real-life" example:

#( task1
NoActive
VARIABLE f-list
: f-action NodeValue ASCIIZ> MsgBox ;
: f-free NodeValue FREE DROP ;
Action:
    ['] faction f-list DoList
    ['] f-free f-list DoList
    f-list FreeList
)#

#( task2
NoActive
Action: f-list OFF FOR-FILES: x:\xxx\*.* IS-DIR? 0= IF FOUND-FULLPATH S>ZALLOC f-list AddNode THEN ;FOR-FILES f-list @ IF task1 RUN THEN )#