Roll Your Own Minilanguages With Mini-Interpreters

Mini-interpreters can do lots of work within a small space, even letting you create your own customized minilanguage. Michael and Dan discuss the pros and cons of mini-interpreters, then give you the tools to roll your own.


September 01, 1989
URL:http://www.drdobbs.com/tools/roll-your-own-minilanguages-with-mini-in/184408206

Figure 1


Copyright © 1989, Dr. Dobb's Journal

Figure 2


Copyright © 1989, Dr. Dobb's Journal

SEP89: ROLL YOUR OWN MINILANGUAGES WITH MINI-INTERPRETERS

ROLL YOUR OWN MINILANGUAGES WITH MINI-INTERPRETERS

Customizing assembler code for speed and readability

Michael Abrash and Dan Illowsky

Michael Abrash and Dan Illowsky are responsible for new products development at Orion Instruments, a Redwood City-based manufacturer of innovative PC-based engineering and instruments. They can be reached at 702 Marshall St., Ste. #420, Redwood City, CA 94063.


When you sit down to program, do you ever think about how nice it would be to have a language written just for your particular application? Although C, Pascal, assembler, and the like are undeniably powerful, they're general-purpose languages; as a result, you spend most of your time matching the general-purpose constructs of those languages to the needs of your particular applications. What if instead you had languages with commands such as "Draw centered text at bottom of screen," "Beep speaker three times and wait for key," or "Animate image left for specified distance?" In that case, simply put, you could concentrate on the functionality rather than the implementation of your applications -- and your code would be one heck of a lot shorter, too.

Although customized languages sound farfetched, in truth they're not, at least not on a small scale. In this article we'll look at mini-interpreters, which let you define small languages designed for specific tasks. Mini-interpreters are easy to create, make for extremely compact programs, are very flexible, and are easily maintained and modified.

We'll start by defining what a mini-interpreter is. After that we'll explore the pros and cons of mini-interpreters, and we'll finish up by looking at a fully functional mini-interpreter. The sample mini-interpreter can draw text and lines and perform text-mode animation, all in less than 700 bytes.

The Flexibility of Assembler-Defined Data

Before you can understand anything else about mini-interpreters, you must understand that the data-definition capabilities of assembler are vastly superior to those of other languages. Only in assembler can you readily create data sequences that consist of arbitrarily intermixed 8-bit unsigned values, 16-bit signed values, 16-bit pointers, 32-bit pointers, and even 64-bit floating-point values.

As an example, the following code defines the contents of memory starting at label AsmData to be a mix of 8-bit signed values, 16-bit pointers, and text:

     AsmData        label    byte
          db        SetXY,79,20
          db        SubProg
          dw        Box4x4$
          db        TextUp,'Enter your name',O
          db        Done

It's important to understand that AsmData in this example isn't just some sort of structure or variant record. The following would be an equally valid definition of AsmData:

     AsmData        label    byte
          db        SubProg
          dw        SpinAround$
          db        TextUp,'Ten seconds until exit...',O
          db        Wait,10
          db        Cls
          db        TextUp,'Exited.',O
          db        Done

AsmData is simply a free-form mix that can contain, in any order, signed and unsigned values of various sizes and near and far pointers. No regularity, repetition, or structure is required, for the point of using assembler-defined data with mini-interpreters is to be able to mix data in any way, unconstrained by the limitations of high-level-language data structures. This makes possible both compact data encoding and complete flexibility in data definition.

Try doing that in C!

How Mini-Interpreters Work

At heart, a mini-interpreter is nothing more than a table-lookup loop driven by a jump table and a free-form sequence of data. The interpreter reads the next function number from the data sequence, advances the data sequence pointer, and calls the jump table entry corresponding to the function number read, as shown in Figure 1. The function called via the jump table may optionally read any number of parameters of any type from the data sequence, advancing the data pointer each time it does so and preserving the data pointer in any case. When the function ends and returns to the mini-interpreter, the next number in the data sequence indicates the next function to be executed. The whole process is repeated until the function that terminates the interpreter is reached.

The key to the operation of a mini-interpreter is that each function knows the number and type of its own parameters, so each function is responsible for reading data from the data sequence and advancing the data sequence pointer properly. The mini-interpreter itself needs to know nothing more than how to read the next function number from the data sequence and call the corresponding function.

In Figure 2, which shows an annotated sample data sequence, note that the mini-interpreter reads only the function number bytes, calling the corresponding function immediately after doing so. Each function is responsible for obtaining and handling its own parameters, thereby leaving the data sequence pointer pointing to the function number for the next function.

The basic operation of a mini-interpreter is simple, then: A data sequence, or "miniprogram," provides function numbers, which are used to vector through a jump table to functions. The function numbers are basically commands in the "minilanguage" defined by the functions in the jump table. The functions then acquire their own parameters from the data sequence as needed.

Another way to view a mini-interpreter is as a control program that allows you to call various functions in any order and with any parameters. The jump table defines the functions that can be called -- in effect defining a minilanguage -- and the data sequence defines the calling order and the parameters, thereby serving as a miniprogram. The same result could, of course, be accomplished simply by writing code that calls the desired routine with the desired parameters. The great advantage of using a mini-interpreter over writing equivalent code is that a mini-interpreter makes for more compact code that's also easier to write and maintain.

Now, consider this: Pointers to the jump table and/or the data sequence can be parameters passed to the interpreter, so the operation of the interpreter can be changed instantly. By passing in a pointer to a different data sequence, the functions in the jump table can be combined in different orders and with different parameters; in other words, a different miniprogram can be run. By passing in a pointer to a different jump table, the very minilanguage that the mini-interpreter supports can be altered.

In other words, not only the m0iniprogram that the mini-interpreter is running but also the minilanguage that it supports can easily be changed, even in mid-program -- the ultimate in flexibility.

Benefits of Mini-Interpreters

The benefits of mini-interpreters are many and varied, with flexibility, compact code, and ease of use being high on the list. Let's look at these benefits in more detail.

As noted earlier, a mini-interpreter is extremely flexible because it consists of nothing more than a function-vectoring loop that's driven by a jump table that defines a minilanguage and a data sequence that defines a miniprogram. A different miniprogram can be executed by running a different data sequence through the mini-interpreter. The function set and/or operation of the functions that make up a minilanguage can be changed at any time simply by altering or replacing the jump table currently driving the mini-interpreter. By the same token, the minilanguage supported by a mini-interpreter can be extended simply by adding additional functions to the jump table.

Not only can a mini-interpreter switch from one minilanguage or miniprogram to another, but it can also nest minilanguages and miniprograms. A minilanguage command can easily save the current data sequence pointer and recursively start the mini-interpreter with a new miniprogram, then restore the original data pointer when the new miniprogram finishes. In effect, this allows mini-interpreters to support subroutines. Similarly, a minilanguage command can save the current jump table pointer and start the mini-interpreter with a new jump table (and a new miniprogram as well, if desired), thereby temporarily switching to another minilanguage altogether.

There really aren't any limitations on the types of commands minilanguages can support. The only rule is that the code that implements any given minilanguage command must preserve the data sequence pointer (the pointer to the current miniprogram), advancing it past any parameters the command uses.

The flexibility of minilanguage function definition leads directly to the next benefit of mini-interpreters, which is compact code. Functions are subject to virtually no limitations, so it's easy to tailor them to perform precisely the tasks that a given application demands, with no wasted code. This means that miniprograms can be very efficient because the commands available in the minilanguage are matched to the task at hand.

Then, too, the function numbers in a minilanguage can easily be encoded in a single byte. This means that all the commands in a minilanguage can be 1 byte long, a claim that not even assembler instructions can make. Again, this is made possible by the narrow focus of a minilanguage. Only the functions needed for a specific task are implemented in a minilanguage, in contrast to general-purpose languages, which must support a wide range of general-purpose programming constructs.

The ability to support 1-byte commands is one area in which mini-interpreters are superior to code that simply calls the desired functions one after another. A function call, as used in C or assembler code, takes a minimum of 3 bytes, in contrast to the 1-byte command encoding used by mini-interpreters.

Another area in which mini-interpreters are superior to code that calls functions is that of passing parameters. Mini-interpreters are extremely compact because there's no overhead involved in passing parameters. A C compiler would (at best) generate the following 8-byte code sequence in order to pass a pointer to the text string HelloMsg as a parameter to the function TextUp%:

     mov     ax,offset     HelloMsg
     push    ax            ;pass the pointer as a parameter
     call    TextUp%
     pop     ax            ;clear the parameter from the stack

Assembler code could be smaller -- but still bulky -- at 6 bytes:

     mov     ax,offset    HelloMsg
     call    TextUp%

In a miniprogram, however, a mere byte of miniprogram code would suffice, with the text built right into the miniprogram:

     db 15,'Hello',O

Here, 15 is the entry number of TextUp% in the jump table that defines the current minilanguage. The last example could be made considerably more readable as follows:

     TextUp    equ    15
               :
     db        TextUp,'Hello',O

We will return to the topic of making miniprograms readable shortly.

Mini-interpreters lend themselves to compact code in every respect. The code in the functions that implement a minilanguage tends to be reused heavily because those functions make up the commands available in the minilanguage. Programs written in the minilanguage can readily be reused in the form of subprograms, which are essentially subroutines, particularly as subprograms can be nested. Any command or subprogram can easily be repeated any number of times simply by defining a function in the minilanguage that starts a nested mini-interpreter the desired number of times.

The code of a mini-interpreter itself can be extremely small and usually is. After all, mini-interpreters don't actually do much; they're just the glue that lets a miniprogram call the functions that make up a minilanguage, as defined by the jump table. Here's the entire mini-interpreter from Listing One which we'll discuss later:

   cld
GetNextCommand:
   lodsb
   mov          bl,al
   xor          bh,bh
   shl          bx, 1
   call         [bx+Function_Table]
   jmp short GetNextCommand

The functions that make up a minilanguage do take up code space, of course, but then you'd have to write that code anyway in order to accomplish the desired task. One of the main points of using a mini-interpreter is to let you sequence those functions and pass those parameters as efficiently as possible.

By the way, although we're only going to discuss assembler mini-interpreters in this article, don't think that mini-interpreters can't be useful in the context of high-level-language programs. For one thing, mini-interpreters can easily be called from high-level-language programs to carry out specific tasks at the cost of far fewer bytes than the high-level language could manage.

What's more, mini-interpreters can even be implemented in high-level languages, albeit not quite so efficiently as in assembler. All that's required to write a mini-interpreter is the availability of pointers and the ability to support jump tables, requirements that C meets admirably. One caveat regarding high-level-language mini-interpreters, though: Always create your miniprograms in assembler, even when your mini-interpreters, jump tables, and minilanguage functions are all written in high-level languages. As we saw earlier, high-level languages can't come close to matching assembler where flexible data definition is concerned -- and flexible data definition is absolutely essential when you want to create the most compact and powerful miniprograms possible.

Ease of Creation and Maintenance

We've yet to cover one characteristic of mini-interpreters, and that's ease of use. You've already seen some of the reasons why mini-interpreters are easy to use: The capabilities of minilanguages are matched to the task at hand, so the available commands are intuitive and fit in without any fuss, and minilanguages are table-driven, so it's easy to add new commands as the need arises. Still and all, at this moment miniprograms might not seem particularly easy to write and maintain, but that's because you haven't yet seen the last piece of the puzzle.

That last piece is the translation of function numbers in miniprograms from numbers to symbols by way of either the equ directive or the macro directive. Assume, for example, that the function SetXY% is the second entry in the jump table for a minilanguage. The miniprogram

     db    2,79,20

isn't particularly easy to write or read -- in fact, it's pretty much incomprehensible. On the other hand, the equivalent miniprogram

     SetXY    equ    2
              :
        db    SetXY,79,20

is easy to write and perfectly readable. Basically, equated symbols are used with mini-interpreters in much the same way that mnemonics are used with assemblers: To allow programmers to work with human-oriented symbols rather than numbers.

If you don't mind losing some assembly speed, you can use macros to make miniprograms still easier to use. Macros can be used both to check the number and type of parameters and to make miniprograms more readable -- for example, the last example can be implemented as follows:

        SetXY       macro    X,Y,ErrorCheck
         ifnb       <ErrorCheck>
                    %out Too many parameters to SetXY
                   .err
         endif
         ifb        <Y>
                    %out Too few parameters to SetXY
                   .err
        endif
        db          2,X,Y
        endm
                    :
        SetXY       79,20

The macro SetXY not only defines the appropriate data for the SetXY command but also checks to make sure that there are exactly two parameters. If necessary, the if directive can even be used to check the magnitude and/or types of the parameters. Once the macro is defined, the actual code of the miniprogram -- SetXY 79, 20 -- is intuitive and easy to read or write. A program written with such macros would look something like

           SetXY                     0,0
           SetXYTextDirection        Down
            TextUp                   'START',
                                     End0fText

which is certainly straightforward enough.

We're not going to use macros in the example program, both because macros take more source code space and because they tend to obscure the basic operation of the mini-interpreter, which is what we're exploring right now. Macros are, however, the best way to go if you need to implement long miniprograms and/or a minilanguage with many commands because parameter error checking can help avoid bugs and the improved readability of macro-based miniprograms can help you find your way around your code.

Limitations of Mini-Interpreters

You might well think that the primary limitation of mini-interpreters is speed -- but you'd be wrong. Interpreters are normally slow, but mini-interpreters aren't normal interpreters in that, unlike most interpreters, mini-interpreters don't have to do any parsing. Miniprograms are already parsed because they consist only of function number bytes and function parameters already in the form -- binary, text, what have you -- that each function expects. The only overhead incurred by the mini-interpreter is reading each function number from the miniprogram and branching to the corresponding entry in the jump table, and that just doesn't take long.

Besides, when you create the functions that make up a minilanguage, you can pack as much functionality as you want into any one function. If there's something that just has to be done as fast as possible, you can create a function that does that task from start to finish as a single mini-interpreter command.

No, speed isn't the major limitation of the mini-interpreter approach; that dubious honor falls instead to decision making. Mini-interpreters can branch in limited ways -- for example, by executing a subprogram or repeating a command multiple times. Mini-interpreters, however, don't lend themselves especially well to the more general sorts of conditional branching and code structures that are needed for decision-making code.

A command that conditionally branches to another miniprogram location is possible, but such a command couldn't easily handle generalized condition testing with relational, logical, and arithmetic operations and would surely lead to cryptic spaghetti code. Code structures such as if. . . then. . . else, for, and do. . . while aren't impossible, but they certainly wouldn't be easy to implement.

Even if they were easy to implement, however, complex control structures are contrary to the reason for using mini-interpreters in the first place, which is efficient implementation of well-defined tasks. If you're going to bother with general-purpose control structures, expression evaluation, and the like, you might as well use a general-purpose language -- that's what those languages are designed for. Mini-interpreters work best when you need to perform tasks that can be expressed as a sequence of parameterized actions.

Don't confuse decision making with complexity -- mini-interpreters are excellent for many sorts of complex tasks. Mini-interpreters save proportionally more space when used for lengthy tasks, and the ease of writing and reading miniprograms matters most when the task is complex.

Applications

So exactly what sort of complex tasks are mini-interpreters suited for? They are suited for complex sound generation, for one, because a miniprogram that could tweak the speaker in various ways and for various periods of time would be far easier to write than, say, assembler code that did the same with a series of out instructions and timer reads. Manipulation of structured data, for another; a minilanguage could readily be built to control insertion, deletion, and modification of records in a database and fields in those records, for example. Parsing text is yet another example, for mini-interpreters lend themselves well to tasks that can be expressed as state machines.

We've saved the most obvious mini-interpreter applications, screen control and animation, for last. Next, we're going to implement a sample mini-interpreter designed for precisely those applications.

A Sample Mini-Interpreter

Listing One shows a fully functional mini-interpreter in action. The sample miniprogram run by this mini-interpreter does quite a bit: It clears the screen, displays the text START and END, draws a complex maze, and animates the movement of an arrow through that maze.

Even though all screen output in Listing One is done through BIOS functions, the entire screen is drawn instantaneously on a PC AT and just a bit more slowly on a PC. In fact, the animation actually must be slowed down considerably by way of the DELAY_COUNT value, so you can see that this num-interpreter provides better than adequate performance. What's more, Listing One assembles to a program just 684 bytes long, with some of those bytes taken by functions that aren't even used in the sample miniprogram. Better yet, the sample miniprogram itself, which starts at DemoScreen$ and ends at SpinAround$, is just 302 bytes long in its entirety.

Mini-interpreters do indeed make for compact programs.

Listing One illustrates many of the desirable features of mini-interpreters. The main miniprogram starting at DemoScreen$ uses subprograms (started with the SubProg command) to make the program still more compact and modular. For example, SpinAround$ is used twice as a subprogram to cause the arrow to spin around a square two characters on a side. If instead we wanted the arrow to spin around a square one character on a side, all we'd need to do is change 2 to 1 in SpinAround$, and the animation of the arrow spinning would be changed through out the program.

It's also interesting to note that DemoScreen$ uses the minilanguage's DoRep command. In fact, it's DoRep that's used to repeat SpinAround$ five times in order to make the arrow spin at the beginning and end of the maze, so you can see that this particular minilanguage supports repetition of subprograms.

It's hard to overstate the flexibility of mini-interpreters such as the one in Listing One. Because the entire drawing and animation sequence can be run by interpreting DemoScreen$, the whole demo could be repeated simply by executing DemoScreen$ as a subprogram, with DoRep repeating everything the desired number of times. If the interpreter were to be started with the following miniprogram:

   db    DoRep,3,SubProg
   dw    DemoScreen
   db    Done

then the entire demo would be repeated three times -- at a cost of just 6 extra bytes.

The miniprogram in Listing One is easy to read, too. The equated names for the various commands are clear, are documented where they're defined with equ, and could be made clearer, if necessary, by lengthening them. As we saw earlier, the minilanguage commands could be implemented as macros if either still greater clarity or parameter checking were needed.

Relatively few commands were required to support the functionality required by this application, so we chose to implement just one minilanguage, and to hard-wire the interpreter Interp for that one minilanguage by directly addressing the minilanguage's jump table Function_Table in Interp. If we had wanted to, however, we could easily have passed the address of the jump table into Interp, thereby allowing Interp to support whatever minilanguage the calling routine chooses, just as it already supports whatever miniprogram the calling routine passes in. Had we done that, we could just as easily have used three minilanguages as one: One minilanguage for text drawing, one for maze drawing, and one for animation.

By the way, Interp can be called recursively; it is in fact called recursively from the SubProg% function, which is invoked with the SubProg miniprogram command. All Interp ever looks at is the current byte in the miniprogram, which is pointed to by si, so starting a subprogram is a simple matter of calling Interp with a new pointer in si. SubProg% pushes the miniprogram pointer -- the pointer to the miniprogram containing the SubProg command -- before calling Interp with a pointer to a subprogram in si. That means that when the subprogram has finished and Interp returns to SubProg%, SubProg% can simply pop si and return to Interp in order to continue execution of the original miniprogram with the command following SubProg.

Function_Table and the functions following Interp in Listing One completely define the minilanguage used in this program and provide all the capabilities available to the miniprogram DemoScreen$. Additional miniprogram code that needed commands not available in Function_Table could be supported by writing the needed functions and adding an entry for each to Function_Table. Additional miniprogram code that could make do with just the functions already in Function_Table would be extremely compact because it could be implemented as nothing more than minilanguage commands, without the need for a single byte of new assembler code.

As a final note, take a look at how the functions following Interp obtain their parameters. Each function gets its own parameters, if any, directly from the miniprogram data pointed to by si, advancing si so that it points to the function number for the next function. Each function is free to obtain parameters in the most efficient possible way; for example, SetXY% loads both the X and Y coordinates from the miniprogram data sequence with a single lodsw.

Conclusion

Mini-interpreters provide a superb way to implement many sorts of well-defined tasks in a minimum of space. Mini-interpreter-based miniprograms are easy to create and maintain, and are extremely flexible. Think of mini-interpreters whenever program size is a consideration; even when space is not an issue, you may want to take advantage of their ease of use.

Availability

All source code for articles in this issue is available on a single disk. To order, send $14.95 (Calif. residents add sales tax) to Dr. Dobb's Journal, 501 Galveston Dr., Redwood City, CA 94063; or call 800-356-2002 (from inside Calif.) or 800-533-4372 (from outside Calif.). Please specify the issue number and format (MS-DOS, Macintosh, Kaypro).

_ROLL YOUR OWN MINILANGUAGES WITH MINI-INTERPRETERS_ by Michael Abrash and Dan Illowsky

[LISTING ONE]



; This program demonstrates the use of a mini-interpreter to produce
; code that is compact, flexible and easy to modify. The mini-
; program draws and labels a maze and animates an arrow through
; the maze.
;
; Note: This program must be run in 80-column text mode.
;
; Tested with TASM 1.0 and MASM 5.0.
;
; By Dan Illowsky & Michael Abrash 2/18/89
; Public Domain
;
Stak    segment para stack 'stack'      ;allocate stack space
        db      200h dup (?)
Stak    ends
;
_TEXT   segment para public 'code'
        assume  cs:_TEXT, ds:_TEXT
;
; Overall animation delay. Selected for an AT: set higher to slow
; animation more for faster computers, lower to slow animation less
; for slower computers.
;
DELAY_COUNT     equ     30000
;
; Equates for mini-language commands, used in the data
; sequences that define mini-programs. The values of these
; equates are used by Interp as indexes into the jump table
; Function_Table in order to call the corresponding subroutines.
;
; Lines starting with ">>" describe the parameters that must
; follow the various commands.
;
Done      equ     0     ;Ends program or subprogram.
                        ;>>No parms.
SubProg   equ     1     ;Executes a subprogram.
                        ;>>Parm is offset of subprogram.
SetXY     equ     2     ;Sets the cursor location (the location at
                        ; which to output the next character).
                        ;>>Parms are X then Y coordinates (both
                        ; bytes).
SetXYInc  equ     3     ;Sets the distance to move after displaying
                        ; each character.
                        ;>>Parms are X then Y amount to move after
                        ; displaying character (both bytes).
SetX      equ     4     ;Sets the X part of the cursor location.
                        ;>>Parm is the X coordinate (byte).
SetY      equ     5     ;Sets the Y part of the cursor location.
                        ;>>Parm is the Y coordinate (byte).
SetXInc   equ     6     ;Sets the X part of the amount to move after
                        ; displaying each character.
                        ;>>Parm is the X amount to move after
                        ; character is displayed (byte).
SetYInc   equ     7     ;Sets the Y part of the amount to move after
                        ; displaying each character.
                        ;>>Parm is the Y amount to move after
                        ; character is displayed (byte).
SetAtt    equ     8     ;Sets the screen attribute of characters to
                        ; be displayed.
                        ;>>Parm is attribute (byte).
TextUp    equ     9     ;Displays a string on the screen.
                        ;>>Parm is an ASCII string of bytes,
                        ; which must be terminated by an EndO byte.
RepChar   equ    10     ;Displays a single character on the screen
                        ; a number of times.
                        ;>>Parms are char to be displayed followed
                        ; by byte count of times to output byte.
Cls       equ     11    ;Clears screen and makes text cursor
                        ; invisible.
                        ;>>No parms.
SetMStart equ     12    ;Sets location of maze start.
                        ;>>Parms are X then Y coords (both bytes).
Mup       equ     13    ;Draws maze wall upwards.
                        ;>>Parm is byte length to draw in characters.
Mrt       equ     14    ;Draws maze wall right.
                        ;>>Parm is byte length to draw in characters.
Mdn       equ     15    ;Draws maze wall downwards.
                        ;>>Parm is byte length to draw in characters.
Mlt       equ     16    ;Draws maze wall left.
                        ;>>Parm is byte length to draw in characters.
SetAStart equ     17    ;Sets arrow starting location.
                        ;>>Parms are X then Y coordinates
                        ; (both bytes).
Aup       equ     18    ;Animates arrow going up.
                        ;>>No parms.
Art       equ     19    ;Animates arrow going right.
                        ;>>No parms.
Adn       equ     20    ;Animates arrow going down.
                        ;>>No parms.
Alt       equ     21    ;Animates arrow going left.
                        ;>>No parms.
DoRep     equ     22    ;Repeats the command that follows
                        ; a specified number of times.
                        ;>>Parm is repetition count (one byte).
;
EndO            equ     0       ;used to indicate the end of a
                                ; string of text in a TextUp
                                ; command.
;********************************************************************
; The sequences of bytes and words between this line and the next
; line of stars are the entire mini-program that our interpreter will
; execute. This mini-program will initialize the screen, put text on
; the screen, draw a maze, and animate an arrow through the maze.
;
DemoScreen$ label byte  ;this is the main mini-program that our
                        ; interpreter will execute
; Initialize the screen
        db SubProg
        dw InitScreen$
; Put up words
        db SetXY,0,0, SetXYInc,0,1, TextUp,'START',EndO
        db SetXY,79,20, TextUp,'END',EndO
; Draw the maze
        db SetMstart,4,0, Mrt,8, Mdn,4, Mrt,4, Mup,3, Mrt,4, Mdn,3
        db Mrt,4, Mdn,8, Mrt,3, Mup,3, Mrt,5, Mup,9, Mrt,17, Mdn,9
        db Mrt,5, Mdn,3, Mrt,4, Mup,10, Mrt,12, Mdn,18, Mrt,6
        db SetXY,4,2, Mrt,4, Mdn,2, Mlt,4, Mdn,18, Mrt,12, Mup,4
        db Mrt,4, Mdn,4, Mrt,11, Mup,11, Mrt,5, Mup,9, Mrt,9, Mdn,9
        db Mrt,5, Mdn,11, Mrt,12, Mup,4, Mrt,4, Mdn,4, Mrt,10
        db SetXY,8,6, SubProg
        dw Box4x6$
        db SetXY,8,14, SubProg
        dw Box4x6$
        db SetXY,24,14, SubProg
        dw Box4x6$
        db SetXY,54,14, SubProg
        dw Box4x6$
        db SetXY,62,4, SubProg
        dw Box4x6$
        db SetXY,16,6, SubProg
        dw Box4x4$
        db SetXY,16,12, SubProg
        dw Box4x4$
        db SetXY,62,12, SubProg
        dw Box4x4$
; Animate the arrow through the maze.
        db SetAStart,3,0, Alt,2, Adn,2, Art,2, Aup,2
        db SetXY,0,0
        db DoRep,5,SubProg
        dw SpinAround$
        db Alt,2, Adn,1, Art,9, Adn,4, Alt,4, Adn,8, Art,8, Adn,8
        db Alt,8, Aup,8, Art,8, Aup,2, Art,8, Adn,2, Art,7, Aup,3
        db Art,5, Aup,9, Art,13, Adn,9, Art,5, Adn,11, Art,8, Aup,10
        db Art,8, Aup,8, Alt,8, Adn,8, Art,8, Adn,10, Art,8, Adn,1
        db Art,2, Aup,2, DoRep,5,SubProg
        dw SpinAround$
        db Alt,2, Adn,1, Art,1
        db Done
; Subprogram to clear the screen and initialize drawing variables.
InitScreen$ db  SetXY,0,0, SetAtt,7, SetXYInc,1,0, Cls, Done
; Subprograms to draw boxes.
Box4x4$    db Mrt,4, Mdn,4, Mlt,4, Mup,4, Mrt,2, Done
Box4x6$    db Mrt,4, Mdn,6, Mlt,4, Mup,6, Mrt,2, Done
; Subprogram to spin the arrow around a square.
SpinAround$ db Alt,2, Adn,2, Art,2, Aup,2, Done
;********************************************************************
; Data for outputting text characters to the screen.
Text_Out_Data   label byte
Cursor_X_Coordinate     db      0
Cursor_Y_Coordinate     db      0
Cursor_X_Increment      db      1
Cursor_Y_increment      db      0
Character_Attribute     db      7
Last_Maze_Direction     db      0ffh ;0-up, 1-rt, 2-dn, 3-lt
                                     ; 0ffh-starting
AnimateLastCoordinates  dw      0    ;low byte is X, high byte is Y
;
; Jump table used by Interp to call the subroutines associated
; with the various function numbers equated above. The functions
; called through this jump table constitute the mini-language
; used in this program.
;
Function_Table label word       ;list of function addresses
        dw      Done%           ; which correspond one for
        dw      SubProg%        ; one with the commands defined
        dw      SetXY%          ; with EQU above
        dw      SetXYInc%
        dw      Set%            ;Set%, MOut%, and Animate% all use
        dw      Set%            ; the function number to determine
        dw      Set%            ; which byte to set or which
        dw      Set%            ; direction is called for
        dw      Set%
        dw      TextUp%
        dw      RepChar%
        dw      Cls%
        dw      SetMStart%
        dw      MOut%
        dw      MOut%
        dw      MOut%
        dw      MOut%
        dw      SetAStart%
        dw      Animate%
        dw      Animate%
        dw      Animate%
        dw      Animate%
        dw      DoRep%
;
; Program start point.
;
Start   proc    far
        push    cs      ;code and data segments are the
        pop     ds      ; same for this program
        mov     si,offset DemoScreen$   ;point to mini-program
        call    Interp                  ;execute it
        mov     ah,1    ;wait for a key before clearing the
        int     21h     ; the screen and ending
        mov     ah,15   ;get the current screen mode
        int     10h     ; so it can be set to force
        sub     ah,ah   ; the screen to clear and the
        int     10h     ; cursor to reset
        mov     ah,4ch
        int     21h     ;end the program
Start   endp
;
; Mini-interpreter main loop and dispatcher. Gets the next
; command and calls the associated function.
;
Interp  proc near
        cld
GetNextCommand:
        lodsb                           ;get the next command
        mov     bl,al
        xor     bh,bh                   ;convert to a word in BX
        shl     bx,1                    ;*2 for word lookup
        call    [bx+Function_Table]     ;call the corresponding
                                        ; function
        jmp short GetNextCommand        ;do the next command
;
; The remainder of the listing consists of functions that
; implement the commands supported by the mini-interpreter.
;
; Ends execution of mini-program and returns to code that
; called Interp.
;
Done%:
        pop     ax      ;don't return to Interp
        ret             ;done interpreting mini-program or subprogram
                        ; so return to code that called Interp
;
; Executes a subprogram.
;
SubProg%:
        lodsw                   ;get the address of the subprogram
        push    si              ;save pointer to where to
                                ; resume the present program
        mov     si,ax           ;address of subprogram
        call    Interp          ;call interpreter recursively
                                ; to execute the subprogram
        pop     si              ;restore pointer and resume
        ret                     ; the program
;
; Sets the screen coordinates at which text will be drawn.
;
SetXY%:
        lodsw
        mov     word ptr [Cursor_X_Coordinate],ax
        ret
;
; Sets the amount by which the cursor will move after each
; character is output to the screen.
;
SetXYInc%:
        lodsw
        mov     word ptr [Cursor_X_Increment],ax
        ret
;
; Sets individual X coordinate, Y coordinate, X movement after
; character is output to the screen,  Y movement, or character
; attribute depending on function number.
;
Set%:
        shr     bx,1            ;calculate the command number
        lodsb                   ; get the new value
        mov     [bx+Text_Out_Data-SetX],al ;store in location
                                           ; corresponding to
                                           ; the command number
Return:
        ret
;
; Displays a string of text on the screen.
;
TextUp%:
GetNextCharacter:
        lodsb                             ;get next text character
        or      al,al                     ;see if end of string
        je      Return                    ;if so, next command
        call    OutputCharacter           ;else output character
        jmp     short GetNextCharacter    ;next character
;
; Displays a single character on the screen multiple times.
;
RepChar%:
        lodsw                    ;get the character in AL
                                 ; and the count in AH
RepCharLoop:
        push    ax               ;save the character and count
        call    OutputCharacter  ;output it once
        pop     ax               ;restore count and character
        dec     ah               ;decrement count
        jne     RepCharLoop      ;jump if count not now 0
        ret
;
; Clears the screen and turns off the cursor.
;
Cls%:
        mov     ax,600h          ;BIOS clear screen parameters
        mov     bh,[Character_Attribute]
        xor     cx,cx
        mov     dx,184fh
        int     10h              ;clear the screen
        mov     ah,01            ;turn off cursor
        mov     cx,2000h         ; by setting bit 5 of the
        int     10h              ; cursor start parameter
        ret
;
; Sets the start coordinates for maze-drawing.
;
SetMStart%:
        lodsw   ;get both X and Y coordinates and store
        mov     word ptr [Cursor_X_coordinate],ax
        mov     [Last_Maze_Direction],0ffh  ;indicate no
        ret                                 ; last direction

;
; Maze-drawing tables.
;
XYincTable      db      0,-1, 1,0, 0,1, -1,0
                        ;X & Y increment pairs for the 4 directions
CharacterGivenDirectionTable db 179,196,179,196
                        ;vertical or horizontal line character to use
                        ; for a given direction
FirstCharGivenNewAndOldDirectionTable label byte
        db      179,218,179,191, 217,196,191,196 ;table of corner
        db      179,192,179,217, 192,196,218,196 ; characters
;
; Outputs a maze line to the screen.
;
MOut%:
        sub     bx,Mup+Mup      ;find new direction word index
        mov     ax,word ptr [bx+XYincTable]       ;set for new
        mov     word ptr [Cursor_X_Increment],ax  ; direction
        shr     bx,1    ;change to byte index from word index
        mov     al,[bx+CharacterGivenDirectionTable] ;get char for
                                                     ; this direction
        mov     ah,al                    ;move horizontal or vert
        mov     dl,[Last_Maze_Direction] ; character into AH
        mov     [Last_Maze_Direction],bl ;if last dir is 0ffh then
        or      dl,dl                    ; just use horiz or vert char
        js      OutputFirstCharacter     ;look up corner character
        shl     dl,1                     ; in table using last
        shl     dl,1                     ; direction*4 + new direction
        add     bl,dl                    ; as index
        mov     al,[bx+FirstCharGivenNewAndOldDirectionTable]
OutputFirstCharacter:
        push    ax                      ;AL has corner, AH side char
        call    OutputCharacter         ;put out corner character
        pop     ax                      ;restore side char to AH
        lodsb                           ;get count of chars for this
        dec     al                      ; side, minus 1 for corner
        xchg    al,ah                   ; already output
        jmp short RepCharLoop           ;put out side char n times
;
; Table of arrow characters pointing in four directions.
;
AnimateCharacterTable   db      24,26,25,27
;
; Animates an arrow moving in one of four directions.
;
Animate%:
        sub     bx,(Aup+Aup)                    ;get word dir index
        mov     ax,word ptr [XYIncTable+bx]     ;set move direction
        mov     word ptr [Cursor_X_Increment],ax
        lodsb                                   ;get move count
        shr     bx,1                            ;make into byte
        mov     ah,[bx+AnimateCharacterTable]   ; index and get
        xchg    al,ah                           ; char to animate
NextPosition:                                   ; into AL, AH count
        mov     dx,[AnimateLastCoordinates]     ;coords of last arrow
                                        ;move cursor to where last
                                        ; character was output
        mov     word ptr [Cursor_X_Coordinate],dx
        push    ax                      ;save char and count
        mov     al,20h                  ;output a space there
        call    OutputCharacter         ; to erase it
        pop     ax      ;restore char in AL, count in AH
        push    ax      ;save char and count
        mov     dx,word ptr [Cursor_X_Coordinate] ;store new coords
        mov     [AnimateLastCoordinates],dx       ; as last
        call    OutputCharacter                 ;output in new
        mov     cx,DELAY_COUNT                  ; location then
WaitSome:                                       ; wait so doesn't
        loop    WaitSome                        ; move too fast
        pop     ax                              ;restore count and
                                                ; character
        dec     ah                              ;count down
        jne     NextPosition                    ; if not done
        ret                                     ; do again
;
; Sets the animation start coordinates.
;
SetAStart%:
        lodsw                                   ;get both X & Y
        mov     [AnimateLastCoordinates],ax     ; coordinates and
        ret                                     ; store
;
; Repeats the command that follows the count parameter count times.
;
DoRep%:
        lodsb                           ;get count parameter
NextRep:
        push    si                      ;save pointer to command
                                        ; to repeat
        push    ax                      ;save count
        lodsb                           ;get command to repeat
        mov     bl,al                   ;convert command byte to
        xor     bh,bh                   ; word index in BX
        shl     bx,1                    ;
        call    [bx+Function_Table]     ;execute command once
        pop     ax                      ;get back the count
        dec     al                      ;see if it's time to stop
        je      DoneWithRep             ;jump if done all repetitions
        pop     si                      ;get back the pointer to the
                                        ; command to repeat, and
        jmp     NextRep                 ; do it again
DoneWithRep:
        pop     ax                      ;clear pointer to command to
                                        ; repeat from stack, leave
                                        ; SI pointing to the next
                                        ; command
        ret
;
Interp  endp
;
; Outputs a text character at the present cursor coordinates,
;  then advances the cursor coordinates according to the
;  X and Y increments.
;
OutputCharacter proc near
        push    ax              ;save the character to output
        mov     ah,2            ;set the cursor position
        mov     dx,word ptr [Cursor_X_Coordinate]
        xor     bx,bx           ;page 0
        int     10h             ;use BIOS to set cursor position
        pop     ax              ;restore character to be output
        mov     ah,9            ;write character BIOS function
        mov     bl,[Character_Attribute] ;set attribute
        mov     cx,1            ;write just one character
        int     10h             ;use BIOS to output character
                                ;advance X & Y coordinates
        mov     ax,word ptr [Cursor_X_Coordinate] ;both x & y Incs
        add     al,[Cursor_X_Increment]           ; can be negative
        add     ah,[Cursor_Y_Increment]           ; so must add bytes
                                                  ; separately
        mov     word ptr [Cursor_X_Coordinate],ax ;store new  X & Y
                                                  ; coordinates
        ret
OutputCharacter endp
;
_TEXT   ends
        end     Start                   ;start execution at Start















Copyright © 1989, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.