TOP | UP: UserTalk Basics | NEXT: Data Manipulation |
A script is just a series of instructions until it actualizes its potential by being executed, also known as running the script. Furthermore, before a UserTalk script can be run, it is always compiled. This chapter describes how compilation and running of scripts takes place. And, since bugs in your script are proverbially as inevitable as death and taxes, you're going to want to isolate and mend those bugs as they crop up; this you can do through Frontier's excellent debugging environment, which this chapter also describes. Finally, we discuss some additional ways in which Frontier helps you figure out what your script does and how to make it do what you want it to.
Before a script can be run or debugged, it must be compiled. Compilation involves translation of the script into tokenized form (p-code) by way of the tables in system.compiler . Compilation is extremely rapid; on most machines, the time involved is typically too small to measure accurately (one- or two-sixtieths of a second).
You compile a script by pressing the Compile button in the script's edit window. If you make changes in the window and then press the edit window's Run button or Debug button without pressing Compile, Frontier compiles implicitly and proceeds. However, the compiled version generated by such implicit compilation is not the same as the compiled version generated by explicit compilation by way of the Compile button.
It is the explicit version that is run when a script is called as a verb. For this reason, if you make changes in a script window and then close the window, Frontier will offer to perform explicit compilation.1
The chief reason for this behavior is that a script can be executed without the user initiating it directly - for example, agent scripts run automatically in the background, and scripts can be triggered by commands from other applications. Scripts can call other scripts, so it is perfectly possible that a script object could be called while you are editing it. The script at that moment might be unfinished, buggy or dangerous. Therefore, it is the explicitly compiled version that runs; thus, there will be no danger as long as the script's current state has never been explicitly compiled.
So the existence of an implicitly and an explicitly compiled version of a script is important and useful. But it can also be confusing. If you modify a script, test it with the Run button, and then execute another script which calls it, the modifications may mysteriously fail to work - because you've forgotten to compile the script explicitly. A particularly insidious variant can arise when you press Run in a script that calls itself: initially, the implicitly compiled version runs, but when it calls itself, the script calls the explicitly compiled version. Then, when you try to track down the problem in Debug mode, debugging itself appears to have broken; the text version of the script is out of synch with the explicitly compiled version that is actually being executed.
The moral is: press the Compile button! On the other hand, if the script is one which might be called automatically, don't press the Compile button until the script is in acceptable shape (see Chapter 27, Agents and Hooks).
NOTE Menu item scripts have no Compile button, because the implicitly compiled version is the only version needed - such a script will never be called in an unexpected automatic way.
The token tables that are located at system.compiler.kernel and system.com-piler.language are called the kernel. A verb is truly "built in" to Frontier if it has a token here, because it is implemented in the Frontier application. All other verbs are implemented as scripts.
The programmer should never call the kernel explicitly. Pre-evaluation takes care of routing calls to the verbs in system.compiler.language.builtins (see Chapter 9, Special Evaluation), and scripts are provided which route calls to the other kernel verbs. So, for example, when you use pack(), pre-evaluation translates it to system.compiler.language.builtins.pack; when you use msg(), the reference is resolved to system.verbs.globals.msg and the script there calls the kernel.
Calls to the kernel happen primarily by means of the kernel() verb. The programmer should never use this verb! It works in unusual ways. It must be always the only command in its script. And it has the remarkable property that the parameters to its caller are passed on automatically. For instance, if you examine system.verbs.globals.msg(), you see that it receives a string parameter, but apparently fails to pass it on when it calls the kernel; that's because it's passing it on by a different mechanism.2
When Frontier compiles a script, it puts up an error dialog if a syntax error prevents compilation. There is a Go To button in this dialog, and if you press it (or hit Enter), Frontier will place the insertion point after the error in the script.
When Frontier does this, it is thinking like a machine, so its idea of where the error is may differ from yours. You may have to hunt back a little to find what a human being would think of as the culprit. For example, if you try to compile the following:
locals (x = 6)
when the error message appears and you press Go To, Frontier places the insertion point after the =, because it is illegal to use this symbol in the parameter list of a verb call. The fact is, of course, that you never intended a verb call: the real problem is that you typed locals for local. Or, if you try to compile:
local (x = target.get()
msg(x)
then when the error message appears and you press Go To, Frontier places the insertion point in the second line, after the left parenthesis; but the problem is in the first line, where a right parenthesis has been omitted.
Several verbs do compilation-related things to script objects as a whole.
script.compile (addrScriptObject)
Identical to pressing the Compile button in the script object's edit window.
To clear the memory occupied by a script's explicitly compiled version:
script.uncompile (addrScriptObject)
The script will take longer to start executing when next called, but some memory is freed up. Chiefly a method of housekeeping; for an example, see suites.samples.basicStuff.windowFormatter() (discussed in Chapter 24, Windows).
To clear a script's text version:
script.removeSource (addrScriptObject)
The script object becomes henceforward permanently uneditable - indeed, it ceases to be a script object, becoming instead a "compiled code" object - but it can still be called. Useful chiefly for security purposes, as a way to distribute a script no one can read. Since you can't read it either, be sure to keep a normal copy. Called by Remove Source Code in the Script menu.
To learn or specify a script's OSA dialect:
script.getLanguage (addrScriptObject)
script.setLanguage (addrScriptObject, languageString)
Identical to using the language popup menu at the bottom of the script object's edit window. OSA dialects are typically "UserTalk" or "AppleScript". On scripts in non-UserTalk dialects, see Chapter 33, AppleScript.
The following are the ways in which script execution may be initiated:3
Debug mode is entered by pressing a script edit window's Debug button. Thereupon, Frontier will propose to treat the script as if you had pressed its Run button, but it pauses with the first executable surface-level command selected and awaits further instructions.
A new set of buttons has now appeared at the top of the window, and you proceed by pressing one of them. (I'll describe what they do in a moment.) Frontier remains in Debug mode until either the script terminates naturally or you exit it with the Kill button. The buttons then return to their normal state.
In the heat of debugging, you might start making changes to a script. You should not continue debugging a script which you have accidentally changed, because the script's text may no longer correspond to the code Frontier is executing, and you'll be confused. Exit Debug mode and start debugging again.
During debugging, there may be several script edit windows, and you might press a button in a window that is not active. It might appear that nothing has happened, which can be confusing. What's going on is that the first click in a non-active window just makes the window active; you thought you were pressing the button, but you weren't.
A breakpoint is a line of a script that is specially marked: it starts with a "stop" hand icon instead of a triangle. The execution path is the complete train of commands that Frontier actually executes during a debugging session as one script calls another.8
In Debug mode, any time the execution path passes through a breakpointed line, Frontier will pause, open, and bring frontmost the script's edit window, and select the breakpointed line, without yet having executed it.
The presence of a breakpoint has no effect when you are not in Debug mode. The database is full of breakpoints left there by the developers of the scripts, but they do not affect normal execution.
To toggle a line's status between normal command and breakpoint, Command-click its triangle or hand, or choose Toggle Breakpoint from the Script menu. You can set a breakpoint at any time, not just in Debug mode. In Debug mode, it's fine to set and remove breakpoints "on the fly"; for example, if you set a breakpoint in a loop after a couple of iterations of the loop, Frontier will pause there on the next iteration (and then you can remove it if you like).
WARNING An on line, an else line, a bundle line, or a line indicating one of the possible values in a case is not an executable line. Neither is a local or a line subordinate to a local, unless assignment takes place there. You can breakpoint such a line, but Frontier will never pause there. You can breakpoint a comment, but this will have no effect, and the hand will not be visible, until the comment is turned to a command line.
We now describe the behavior of the Debug mode buttons. The following two rules should be borne in mind:
The Go button causes the script to continue execution without pausing until either all execution terminates (at which point Frontier leaves Debug mode) or a breakpoint is encountered. The Go button changes to Stop during execution; the Stop button causes a pause.
The Step button causes execution of the currently selected line, pausing before the next line to be executed. If the current line contains a verb call, the execution path enters and returns from the called script or handler without pausing (unless a breakpoint is encountered).
The In button causes the script to continue execution without pausing (unless a breakpoint is encountered) until a verb call is executed (Frontier pauses at the first executable line of the called handler or script) or until the current handler or script terminates.
The Out button causes the script to continue execution without pausing (unless a breakpoint is encountered) until the current handler or script terminates.
The Follow button causes the script to continue execution without pausing (unless a breakpoint is encountered), and each line of any open script through which the execution path passes is selected as it is executed. This makes it possible to watch the execution path, but no windows are opened, nor are windows brought to the front as the execution path passes through them; also, the speed is rather fast, and cannot be changed. The Go button changes to Stop during execution; the Stop button causes a pause.
The Kill button aborts execution and brings Frontier out of Debug mode. You can use Command-period instead.
In Debug mode, the value of local variables can be consulted. This is actually just a consequence of the fact that it is possible to pause. During execution, as Frontier encounters each new scope, it creates a subtable in system.compiler. stack and maintains there the names and values of any local variables created in that scope; when that scope goes out of existence, so does the corresponding subtable. In Debug mode, you can examine these subtables.9
The most convenient way to examine local variable values in Debug mode is to push the Lookup button. If the Lookup button is pressed while a line as a whole is selected, the subtable for that scope opens. If the Lookup button is pressed while the name of a variable is selected, the subtable containing that variable opens; or you can get the same effect by Command-double-clicking the name.
Navigating the subtables by hand is also possible. Jump to system.compiler. stack and find the subtable containing the variable you wish to examine. The name of a subtable tells which scope within what script it represents. It may take some hunting to find a desired variable.
You can take advantage of a pause to examine other parts of the database as well. If the Lookup button is pressed while the name of a database object is selected, that object opens or is revealed; or you can get the same effect by Command-double-clicking the name.
When a runtime error is not trapped by a try, Frontier puts up an error dialog.
Even when Frontier is not in Debug mode, this error dialog can be used to help track down the error. Clicking the Go To button (or hitting Enter) opens the script window and puts the selection point at the line that caused the error. Or, holding down the Go To button pops up a menu which shows the calling chain; you can use this information to help figure out what was going on when the error occurred, as well as to navigate to the various handlers and scripts named in the menu.
If Frontier is in Debug mode, exactly the same thing happens, but the compiler's variable stack is not emptied, so you can also examine variable values. However, you can't continue debugging; after all, an error has occurred. So after you've finished examining things, you must exit Debug mode.
Since the compiler stack is emptied when a runtime error occurs not in Debug mode, a useful strategy is then to run the same routine in Debug mode so that variables can be examined when the error occurs again.
TIP The text of the error dialog can be copied to the clipboard by choosing Copy when it is frontmost.
Runtime errors are by nature often pesky to track down, even with all the help Frontier gives you. It can be useful to modify the code temporarily to be more informative. A judicious sprinkling of try...else structures and msg() or dialog.notify() calls can help you work out what was happening when the error occurred.
To abort a running operation, hit Command-period. You may have to hit it several times or hold it down.
If you think the database may have been damaged, choose Revert from the File menu, or quit without saving the database. If you're still worried about its integrity, you can check whether Frontier has lost the internal consistency of any tables by choosing UserLand Testing from the Suites menu and then Validate Tables from the Test menu that appears. But this won't tell you if you have overwritten or deleted some important value.
The best policy is to back up the database from time to time. Personally, I also keep a spare copy of a completely clean root downloaded from UserLand's Web site. That way, if I suspect the integrity of the database, I can migrate my personal stuff into the clean root. These matters are all discussed in Chapter 29.
DocServer is an online help program included with the Frontier distribution. It contains documentation for two kinds of UserTalk features: (1) punctuation, operators, and keywords; and (2) verbs. It's very useful, though, as of this writing, the information it contains has some slight drawbacks. Not every built-in verb is documented, and important utility verbs and UCMDs are omitted. The documentation for some keywords fails to describe their full syntax. The documentation is sometimes unnecessarily obscure, and occasionally just plain wrong.
Also, the DocServer application is hard to navigate: the Index menu does not include every category of verb; no menu lists other verbs in the present category, and if you don't know the exact name of a verb, there are no good facilities for finding it.
Nonetheless, Frontier and DocServer work tightly together to provide a handy reference, and it is extremely useful to be conversant with the ways in which they do so:
Exploration of the database is a primary debugging (and learning!) technique. If a verb is implemented as a script, you can study that script. You can jump to any database entry whose name you see in an edit window by Command-double-clicking that name, and you can jump to any database entry whose name you know by choosing Jump from the Open menu. In both cases, partial object references are resolved for you.
1. When Frontier starts up, a script has no explicit compiled version, so Frontier will make one automatically the first time the script is called. Therefore, if you make changes to a script just after startup and close the script window, no compile prompt appears.
3. Calling a script object from another script is not included, because by definition the caller is already somehow running; the question is how that might ultimately have been caused.
6. This overwrites anything displayed in the Main Window during the evaluation; you may wish to modify this behavior. A MacBird card (a binary object of type 'CARD') can also be run by selecting it in a table and choosing Run Selection.
7. This overwrites anything displayed in the Main Window during the evaluation; you may wish to modify this behavior.
8. For purposes of debugging, the execution path is never considered to pass through any script which consists solely of a call to kernel().
TOP | UP: UserTalk Basics | NEXT: Data Manipulation |