projects:yet_another_interpreter_organization
\ From Sun Forth doc file by Mitch Bradley Sep 20 21:55 1984 interpreter.doc Page 1 Yet Another Interpreter Organization There has been a mild controversy in the Forth community about how to implement the text interpreter. The particular problem is how the distinction between compiling and interpreting should be coded. At least three distinct solutions have been advocated over the years. I propose a fourth one, and claim that it is the best solution yet. [ Additional extract from a Unixnews item from Mitch: "I have modified the compiler so that it doesn't stop when it hits an undefined word; instead, it prints an error message and compiles a reference to a word LOSE, whose run time action is to complain and abort. This scheme allows the compiler to find all the undefined words in a file, without making such errors propagate through all words that use the undefined word. This is especially wonderful in an environment where you can run the editor and the forth process simultaneously in separate windows, such as inside Gosling's Emacs or in the SunWindows environment." ] FIG-Forth Solution FIG-Forth used a variable STATE whose value was 0 when interpreting and (hex) C0 when compiling. The interpreter was coded as a single word INTERPRET which tested STATE to determine whether to compile or to interpret. Here is the code: : INTERPRET ( -- ) BEGIN -FIND IF STATE @ < IF CFA , ELSE CFA EXECUTE THEN ELSE HERE NUMBER DPL @ 1+ IF DROP [COMPILE] LITERAL ELSE [COMPILE] DLITERAL THEN THEN ?STACK AGAIN ; The "STATE @ <" phrase is pretty clever (or disgusting, however you wish to look at it). Since the value stored in STATE is (hex) C0 when compiling, and since the length byte of a defined word (which is left on the stack by -FIND) is in the range (hex) 80-BF for a non-immediate word and in the (hex) C0-FF for an immediate word, the "STATE @ <" test manages to return TRUE only if the STATE is compiling and the word is not immediate. This fact is not salient to our discussion, but is included here to prevent confusion. STATE is explicitly tested once inside this loop, but if you look at the code for the word LITERAL, it too tests STATE to decide whether to compile the number or not. To switch between compilating and interpreting, Fig-FORTH uses two words [ and ]. [ is immediate and simply stores 0 into STATE. ] is not immediate and stores (hex) C0 into STATE. Compilation is typically started with : (colon), which is defined something like: : : <some irrelevant stuff> ] ;CODE <some assembly language stuff> END-CODE The important point here is that when : executes to define a new word, the ] just sets the STATE to compiling, then the ;CODE proceeds to execute. (The purpose of ;CODE is to patch the code field of the word defined by : so that it does the appropriate thing for a high-level forth word). The interpret word INTERPRET doesn't notice that STATE is now compiling until the ;CODE finishes. So we see that [ and ] are pretty innocuous; they just change the value of a variable. Sep 20 21:55 1984 interpreter.doc Page 2 Poly-FORTH Solution Forth, Inc. decided that it would be better to have two separate loops for the two separate functions of compiling and interpreting. The compiling loop was called ], so ] actually executed the compile loop directly, rather than just setting a variable. This has two subtle side effects. If you loop at the previous definition of :, and now pretend that instead of just setting a variable, ] actually executes the compiler loop, you will see that the ;CODE following it doesn't actually get executed until AFTER the compiling is finished. This in itself doesn't cause a problem for :, but the use of ] inside programmer-defined words sometimes caused unexpected behavior because stuff after the ] would get executed after a bunch of stuff had been compiled. The other subtlety relates to how the loops are terminated. Note that the INTERPRET loop shown above never terminates! We all know that it really does terminate, and the mechanism is pretty kludgey. What happens is that there is a null character at the end of every line of text in the input stream, and at the end of every BLOCK of text from mass storage. The text interpreter picks up this null character just like a normal word. The dictionary contains an entry which matches this "null word". The associated code is executed, and it plays around with the return stack in such a way that the INTERPRET loop is exited without its ever knowing about it. The problem with the dual-loop interpreter/compiler is that the end of each line of input from the input stream kicks out system out of whichever loop it was in. If the user is attempting to compile a multi-line colon definition from the input stream, he must start each line after the first with an explicit ], because once the compiler loop is exited at the end of the first line, the system doesn't remember that it was compiling. One key thing to remember is that the compiler loop (which was named [) is executed from within the interpreter loop. Coroutines (Patton/Berkey) At FORML 83, Bob Berkey presented a paper about using coroutines for the interpreter loop and the compiler loop, instead of having the compiler loop run inside the interpreter loop. This means that executing ] kicks out the interpreter loop and runs the compiler loop instead; similarly, executing [ kicks out the compiler loop and runs the interpreter loop instead. The subroutine versions of these loops are present in his scheme, named COMPILER and INTERPRETER. Bob feels that this scheme is more symmetrical than the Poly-FORTH approach, and that it eliminates some of the counter-intuitive behavior. This scheme still requires that multi-line colon definitions compiled from the keyboard have a ] at the beginning of each line after the first. What is Wrong with all this These different schemes do not at all address what I consider to be the fundamental problems with the interpreter/compiler. Fundamental Problem #1: The compiler/interpreter has a built-in infinite loop. This means that you can't tell it to just compile one word; once you start it, off it goes, and it won't stop until it gets to the end of the line or screen. Sep 20 21:55 1984 interpreter.doc Page 3 Fundamental Problem #2: The reading of the next word from the input stream is buried inside this loop. This means that you can't hand a string representing a word to the interpreter/compiler and have it interpret or compile it for you. Fundamental Problem #3: The behavior of the interpreter/compiler is hard to change because all the behavior is hard-wired into one or two relatively large words. Changing this behavior can be extremely useful for a number of applications, for example meta-compiling. Fundamental Problem #4: If the interpreter/compiler can't figure out what to do with a word (it's not defined and it's not a number), it aborts. Worse yet, the aborting is not done directly from within the loop, but inside NUMBER. This severly limits the usefulness of NUMBER because if the string that NUMBER gets is not recognizable as a number, it will abort on you. (The 83 standard punts this issue by not specifying NUMBER, except as an uncontrolled refernece word). Solution: As I see it, there are several distinct things that are going on inside the interpreter/compiler. A proper factorization of the interpreter/compiler into words which each do one thing solves all these problems. The outermost thing is the loop. The loop's job is to repetitively get the next word from the input stream and do something with it. The loop should terminate when the input stream is exhausted. : NEW-INTERPRET (S -- ) BEGIN BL WORD ( str ) MORE? ( str f ) ( flag true if input stream not exhausted ) WHILE "COMPILE REPEAT DROP ; The next level down is the "do something with it". This ought to be a separate word so that it may be called by other words which would like to compile/interpret a single word. This layer is here called "COMPILE, because it takes a string representing a single word and compiles (or interprets) it. "COMPILE's main job is to decide what kind of word it is dealing with. There are 3 choices. Either the word is already defined, or it is a literal (i.e. a number), or it is neither. : "COMPILE ( str -- ?? ) FIND ( str 0 | cfa -1 | cfa 1 ) DUP IF DO-DEFINED ( ?? ) ELSE DROP ( str ) LITERAL? ( str false | ?? true ) IF DO-LITERAL ( ?? ) ELSE DO-UNDEFINED ( ?? ) THEN THEN ; Sep 20 21:55 1984 interpreter.doc Page 4 Finally, at the lowest layer, there is the code which does the appropriate thing for each of these three possibilities. This level is represented by the words DO-DEFINED, DO-LITERAL, and DO-UNDEFINED. It is ONLY at this lowest layer that the system cares at all whether it is compiling or interpreting. One of the benefits claimed for the Poly-FORTH scheme is speed. This is due to the elimination of tests of the STATE variable within the loop. Clearly, my scheme has to do something to distinguish between compiling and interpreting. An obvious solution would be to test STATE inside each of DO-DEFINED, DO-LITERAL, and DO-UNDEFINED. This would slow down the system, of course. A more interesting alternative is to make each of DO-DEFINED, DO-LITERAL, and DO-UNDEFINED a deferred word. (Deferred words are sometimes called execution vectors. Basically they are like variables which hold the address of a word to execute, except that the @ EXECUTE is done automatically) If these words are deferred, then they can be changed when the system goes from compiling to interpreting, and vice versa. DEFER LITERAL? ( str -- n true | d true | str false ) DEFER DO-DEFINED ( cfa -1 | cfa 1 -- ?? ) DEFER DO-LITERAL ( literal -- ?? ) DEFER DO-UNDEFINED ( str -- ) : (LITERAL? ( str -- str false | literal true ) >R R@ NUMBER? ( l f ) IF R> DROP TRUE ELSE DROP R> FALSE THEN ; ' (LITERAL? IS LITERAL? : INTERPRET-DO-DEFINED ( cfa -1 | cfa 1 -- ?? ) DROP EXECUTE ; : COMPILE-DO-DEFINED ( cfa -1 | cfa 1 -- ) 0> IF EXECUTE ( if immediate ) ELSE , ( if not immediate ) THEN ; : INTERPRET-DO-LITERAL ( d -- d | n ) DOUBLE? 0= IF DROP THEN ; : COMPILE-DO-LITERAL ( d -- ) DOUBLE? IF [COMPILE] DLITERAL ELSE [COMPILE] LITERAL THEN ; : INTERPRET-DO-UNDEFINED ( str -- ) COUNT TYPE ." ?" CR QUIT ; : COMPILE-DO-UNDEFINED ( str -- ) COUNT TYPE ." ?" CR COMPILE LOSE ; Then [ and ] would be defined as follows: : [ ['] INTERPRET-DO-DEFINED IS DO-DEFINED ['] INTERPRET-DO-LITERAL IS DO-LITERAL ['] INTERPRET-DO-UNDEFINED IS DO-UNDEFINED Sep 20 21:55 1984 interpreter.doc Page 5 STATE OFF ; IMMEDIATE : ] ['] COMPILE-DO-DEFINED IS DO-DEFINED ['] COMPILE-DO-LITERAL IS DO-LITERAL ['] COMPILE-DO-UNDEFINED IS DO-UNDEFINED STATE ON ; (IS is the word which sets the word to execute for a deferred word. Executing a deferred word need not be slow. Deferred word are so useful that they should be coded in assembler for speed. On my system they are only very slightly slower than normal colon definitions. So what? This may seem to be more complicated than the schemes it replaces. It certainly does have more words. On the other hand, each word is individually easy to understand, and each word does a very specific job, in contrast to the old style, which bundles up a lot of different things in one big word. The more explicit factoring gives you a great deal of control over the interpreter. Here are some interesting things you can do with this new scheme: One of my favorite words, TH (for Temporary Hex): : TH ( --word ?? ) BASE @ >R HEX BL WORD "COMPILE R> BASE ! ; IMMEDIATE This word temporarily sets the base to hexadecimal, interprets a word, and restores the base. It works for numbers or defined words, either interpreting or compiling. For example: DECIMAL TH 10 . (system prints--> 16 10 TH . (system prints--> A : STRIP-PARITY ( char -- char-without-parity ) TH 7F AND ; Liberal use of this word markedly reduces the need to switch bases, especially in source code, and thus reduces the chance of errors. Here's a common word that is trivial to implement with this kind of interpreter: : ASCII ( --name char ) BL WORD 1+ C@ ( char ) -1 DPL ! \ make sure it's not handled as a double number DO-LITERAL ; Here's a word which allows you to make a new name for an old word. It is a smart word, in that when the new word is compiled, the old word will Sep 20 21:55 1984 interpreter.doc Page 6 actually be compiled instead, eliminating any performance penalty. Furthermore, it even works for old words that are immediate! As you will see, the vectored "DO-DEFINED" does exactly the thing we want. : ALIAS ( -- ) ( Input stream: new-name old-word ) CREATE BL WORD FIND ( cfa -1 | cfa 1 | str false ) DUP IF , , IMMEDIATE ELSE DROP ." Can't find " COUNT TYPE THEN DOES> 2@ ( cfa -1 | cfa 1 ) DO-DEFINED ; ( Examples ) ALIAS D@ 2@ HERE D@ ( actually executes 2@ ) : FOO HERE D@ ; ( actually compiles 2@ ) ALIAS FOREVER AGAIN : LOOP-ALWAYS BEGIN FOREVER ; ( actually executes AGAIN, which is immediate ) Finally, a really neat way to write keyword-driven translators. Suppose you have some kind of a file that contains a bunch of text. Interspersed throughout the text are keywords that you would like to recognize, and the program should do something special when it sees a keyword. For things that aren't keywords, it just writes them out unchanged. Suppose that the keywords are ".PARAGRAPH", ".SECTION", and ".END". VOCABULARY KEYWORDS DEFINITIONS : .PARAGRAPH ( whatever you want to happen when you see paragraph ) ; : .SECTION ( whatever you want to happen when you see paragraph ) ; : KEYWORDS-DO-UNDEFINED ( STR -- ) COUNT TYPE ; : .END ONLY FORTH ['] (LITERAL? IS LITERAL? ['] INTERPRET-DO-UNDEFINED IS DO-UNDEFINED ; ONLY FORTH ALSO KEYWORDS : PROCESS-KEYWORDS ['] FALSE IS LITERAL? ['] KEYWORDS-DO-UNDEFINED IS DO-UNDEFINED ONLY KEYWORDS ; I have used this technique very successfully to extract specific information from data base files produced by a CAD system. Instead of outputting unrecognized words, I actually just ingored them in this application, but the technique is the same in either case. Mitch Bradley Sun Microsystems, Inc. 2550 Garcia Ave. Mountain View, CA 94043 (Work) 415/960-3300x314 (Home) 415/961-1302
projects/yet_another_interpreter_organization.txt · Zuletzt geändert: 2013-06-06 21:27 von 127.0.0.1