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]
Copyright © 1989, Dr. Dobb's Journal<a name="01da_000d">
; 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