Compiling and Decompiling

Before an AppleScript program can be run, it must be compiled. To compile something means to transform it, behind the scenes, from text—the form in which a human user is able to read and edit the code—to a form consisting of actual executable instructions intended for a machine.

Compiling

The nature of compiled code depends upon the nature of the engine that runs it. In the case of a C program written for a Macintosh, the engine is the Macintosh’s central processing unit (CPU); so the compiled code must consist of machine-language instructions in accordance with the CPU chip’s specifications. Such machine-language instructions, indeed, are the only instructions your computer can really execute, because the computer’s ultimate “brain” is the CPU.

In the case of AppleScript, the engine is the AppleScript scripting component, and compiled code does not consist of anything so low-level. AppleScript compiled code is bytecode : roughly, the nouns and verbs of the original text are translated into some sort of compressed, coded equivalent (called tokens ). These tokens are meaningful only to the AppleScript scripting component’s run-engine. When it runs the code, the engine still has to parse whatever tokens it meets along its path of execution, accumulating them into chunks and translating these chunks further in order to execute them. It is usual to describe a language that is compiled and executed in this way as being interpreted .

There isn’t necessarily anything shameful about being an interpreted language. (After all, Java is an interpreted language, and yet some people take it seriously.) Being an interpreted language does mean that running AppleScript code is relatively slow, since the compiled code must be processed further while being run before it can really be run, and there is a heavy layer of operation (the runtime engine) between the compiled code and the CPU. Whether you’ll perceive this slowness depends on the nature of the script and the speed of your computer; typically the observable bottleneck will be the time required for communicating with the target scriptable application and for that application to process commands and queries, not the overall speed of the AppleScript runtime engine.

If running even a compiled script is slow, why bother to compile beforehand? Well, for one thing, the nature of bytecode is such that it can be executed in linear fashion; tokens are not interpreted until and unless execution reaches them, and they can be interpreted by gathering them up in order. With raw text, on the other hand, you could be saying anything at all—you could be saying nonsense—and AppleScript would have to work very hard to find this out. By doing that hard work at compile time, not at execution time, AppleScript makes execution faster. The AppleScript compiler is what’s called a single-pass compiler ; this is a fairly simple-minded approach to compilation, but it helps to ensure that your script has a certain level of legality and overall consistency before runtime. (Of course, even a legal, successfully compiled script could still choke at runtime, but this would be for a different kind of reason; we’ll see many examples in the course of the book.)

Also, the use of compilation makes AppleScript a better language. For instance, the following AppleScript code is legal:

sayHowdy(  )
on sayHowdy(  )
        display dialog "Howdy"
end sayHowdy

In this code, we call a handler, sayHowdy( ), before we’ve defined it. If AppleScript code were not compiled ahead of time, this would not be possible, because upon encountering this reference to a handler it has not yet met, the runtime engine simply wouldn’t know what to do.

Decompiling

In the Script Editor, an uncompiled script, or those regions of the script that have been edited since the last compilation, will appear in a single font and size without any automatic indentation. A compiled script is pretty-printed ; different kinds of word may appear in various colors, fonts, and sizes (depending upon your formatting preferences), and there is automatic indentation in accordance with the script’s structure. This pretty-printing is performed by the AppleScript scripting component; the Script Editor merely shows it to you, by asking the AppleScript component for the compiled version in a form suitable for display to a human user.

The AppleScript scripting component, however, remembers the compiled script as bytecode, not as human-readable text. In order to respond to the Script Editor’s request to supply the compiled script in human-readable form, the AppleScript scripting component must transform the bytecode into text. This is called decompiling. So the pretty-printed text that you see when you compile your text in the Script Editor is not merely a colorful version of your text; it’s a completely different text, supplied by decompiling the compiled script.

A curious consequence of this is that some of the words constituting your script may differ before and after compilation. For example, suppose you type this code:

tell app "Finder" to get ref disk 1

It compiles, but in the process it is transformed into this:

tell application "Finder" to get a reference to disk 1

The reason is that some AppleScript terms have synonyms that are acceptable for purposes of compilation, but the scripting component does not remember, in the compiled bytecode, that you used one of these synonyms. It substitutes, at compile time, a token signifying the canonical form of the term; thus, when it decompiles the bytecode, what you see is the canonical form.

When AppleScript reformats your script in the course of pretty-printing it, it may render it harder to read rather than easier. For example, AppleScript allows you to break long lines into shorter lines through the use of a continuation character (¬ ); at compile time AppleScript will sometimes undo your attempts to use this feature, removing your continuation characters or putting them somewhere else. If I compile this code:

do shell script "echo 'hi'" ¬
        password "myPassword"

AppleScript rebreaks it like this:

do shell script ¬
        "echo 'hi'" password "myPassword"

This looks like a trivial annoyance, but if the line were longer it wouldn’t be so trivial. The problem stems from the fact that the bytecode contains no information about whitespace, so the new formatting imposed by the decompilation process may not correspond to your original whitespace. Fortunately, the new version of the Script Editor wraps long lines, which makes this much less of a problem than it used to be.

External Referents Needed at Compile Time

AppleScript is a little language, leaving it up to various external entities such as scriptable applications (or scripting additions) to extend the language as needed. When the time comes to compile a script, if it makes any use of such externally defined extensions to the language, those external entities must be present, and AppleScript must be able to locate them, so that it can ascertain whether the words you’re using in your code correspond to extensions to the language defined in these external entities, and if so, how to translate them into bytecode.

To see what I mean, let’s start with code like this:

get disk 1

If that’s all your script says, it won’t compile at all, because the notion “disk” used as a class (which is how it’s being used here) isn’t part of the AppleScript language. You can use this phrase only with reference to some external entity, such as a scriptable application, that does define the notion “disk” used as a class, as part of its way of extending the AppleScript language. Now, suppose we just make up such an application:

tell application "NoSuchApp" to get disk 1

When you try to compile this, you’re presented with a dialog listing every application, and posing the question: “Where is NoSuchApp?” AppleScript has realized that although, in the AppleScript language proper, the notion “disk” is meaningless the way you’re using it, it might be meaningful in the context of this application NoSuchApp. AppleScript therefore attempts to find the application. (I do not know the exact steps used in the search.) But NoSuchApp can’t be found, because there’s no such app. If you cannot at this moment provide AppleScript with a pointer to NoSuchApp, the script will simply not compile.

Interestingly, you can nominate any application as NoSuchApp; it doesn’t have to be called NoSuchApp. This makes sense because the problem might merely be that the name has changed or you’ve typed it wrong. For example, you could claim that NoSuchApp is Address Book. However, having investigated Address Book’s vocabulary, AppleScript concludes that Address Book doesn’t know what a “disk” is, and the script still won’t compile.

Now suppose we try again, and this time, when AppleScript asks us about NoSuchApp, we tell it that NoSuchApp is the Finder. The script now compiles successfully, because the Finder does know what a “disk” is; and in the compiled script (that is to say, the decompiled script) the name “NoSuchApp” is changed to “Finder”.

To repeat: you cannot compile a script in the absence of the necessary external entities. AppleScript will look for these entities, and will consult you for assistance if it can’t find them; but if neither AppleScript nor you can locate them, that’s the end of the story.

We’ll return to the business of how AppleScript knows about particular applications’ privately defined vocabulary, later in this chapter (Section 4.8) and again later in the book (Chapter 19).

Saving Compiled Scripts

Compilation takes some time—several seconds, in the case of a lengthy script. Thus, with a script that will not change and that one intends to execute at some future time, it would be nice, at runtime, to skip compilation altogether, avoiding any initial delay and permitting the runtime engine to leap into action immediately. The current instance of the AppleScript scripting component remembers the compiled script and is ready to run it at a moment’s notice; but the current instance of the AppleScript scripting component goes out of existence when we quit the host application (such as the Script Editor). Therefore we would like a way for the compiled script to outlive the current instance of the AppleScript scripting component. AppleScript allows this; it is possible to save a script in its compiled form. The result is a compiled script file (see Section 4.3.5 earlier in this chapter).

Tip

A saved compiled script file has extension .scpt on Mac OS X and is of type 'osas' on previous systems. The Script Editor saves such a file with both features, and the resulting file can be opened, edited, and executed on both Mac OS X and Mac OS 9 (the latter only if the version of Script Editor is sufficiently recent). There is also a new "script bundle” format with extension .scptd, which is not backward-compatible to before Mac OS X 10.3 (“Panther”).

The reason for the caveat about the version of the Script Editor is that there are two ways to save the compiled script data—in the resource fork or in the data fork. Early versions of the Script Editor saved it as a resource. Recent versions of the Script Editor save it in the data fork, and you don’t have to go back very far in time before you get to a version of the Script Editor that can’t cope with this. So there are really three compiled script file formats: the old format with the script data in the resource fork, the new format with the script data in the data fork, and the really new format with the script data as a file inside a bundle. This is something to be careful of. We already saw in Chapter 2 that REALbasic couldn’t read the new formats and that Xcode’s NSAppleScript class couldn’t read the old format.

In the Script Editor, when you save a script, you’re offered a choice of format in which to save it: you can save it as text (or “script text,” depending what version of the Script Editor you’re using), or as a script (a compiled script file). The former saves just the current text appearing in the window, even if not yet compiled. The latter attempts to compile the script if it contains any uncompiled changes, and then, if compilation was successful, saves the bytecode (also called the script data). You can confirm this by getting a hex dump of a saved compiled script:

$ vis -otw -F 60 myScript.scpt
FasdUAS\0401.101.10\016\000\000\000\004\017\377\377\000\
\001\000\002\000\003\001\377\377\000\000\015\000\001\000\
[... and so on ...]

Nothing legible here. It’s bytecode, all right. But of course when you open this compiled script in the Script Editor, it shows up legibly. The mechanism, as you have surely guessed, is that the saved bytecode is handed to the AppleScript scripting component, and then decompiled for display.

You cannot save as a compiled script file code that, for whatever reason, will not compile. This seems tautological, but it can be surprising nevertheless, so it is worth mentioning.

References to applications

When you open a compiled script file, and the saved bytecode is handed to the AppleScript scripting component, and the scripting component decompiles the script for display, the script faces issues with regard to any external information parallel to what happens when it is compiled originally. If a compiled script targets a particular application, that application must be located; if it cannot be located, the compiled script can’t be decompiled, and it can’t be opened in the Script Editor.

Enough information about the application is stored with the compiled script to enable the AppleScript scripting component to find it under most circumstances. The compiled script contains an alias to the application; an alias is a very clever kind of pointer, such that if you change the application’s name, or move it to a different folder on the same volume, the compiled script will continue to keep track of the application, and will open with no problem in the Script Editor. But if you move the application to a different volume, the compiled script may lose track of it, so that when you try to open it in the Script Editor, you’ll get the dialog asking you to locate it. If you don’t, the compiled script won’t open. If you do, then the compiled script will open; the script is now modified within the AppleScript scripting component, so that the reference to the application points at it in its new location.[3] There’s more about this decompilation process, and about what happens if you lie to AppleScript and nominate the wrong application as the missing application, in Chapter 19.

In many situations, you can hand a compiled script file over to some application for execution without asking that the text of the script be displayed to you—that is, without decompiling it. (See, for some examples, Section 2.4.) As the compiled script executes, if it refers to externals, it may face some issues parallel to those that it would face at compilation and decompilation time; but these issues do not arise until execution is underway and code that targets an application is actually encountered. If at that point the AppleScript scripting component can’t find the target application, what happens depends upon the context. Ideally we would like to be presented with the dialog asking for the application, and this does usually happen; but sometimes it doesn’t. In the case of the Script Menu, for example, a script that targets an application that can’t be found will simply fail silently. Furthermore, having helped the AppleScript scripting component find the application, you’d like that information to be saved back into the script so that next time the script will run without assistance. Again, sometimes this happens and sometimes it doesn’t, depending upon the context.

Run-only scripts

A compiled script can optionally be saved as run-only. Normally, a compiled script actually contains two kinds of information:

  • The tokens (bytecode) needed to run the script

  • Further information needed to decompile the script and display it to the user

For example, let’s say you put a comment into your script. This comment is nothing that the runtime can execute; that’s what it means to be a comment. But clearly you don’t want this comment thrown away merely because you compile the script; the bytecode retains it, so that when the script is decompiled, you can still read the comment. Similarly, the names of variables are intended entirely for humans; as far as bytecode is concerned, it would be sufficient to assign them numbers, and that’s probably what AppleScript bytecode does. But you don’t want your variable names to be lost, so they are saved as part of the bytecode, even though they aren’t needed for execution.

When you save a compiled script as run-only, the tokens needed merely to decompile the script and display it to the user are thrown away. This makes the compiled script much smaller and probably causes it to run a bit faster, but the compiled script can never again be displayed to the user. If you have not saved another copy, you will never again be able to read or edit that script.

If a run-only script loses track of an external referent, such as an application, there is no way to show the script where the referent is and recompile. In the case of an application reference, the dialog asking where the application is will appear, the user can then show AppleScript the application, and the script will then continue executing; but this information cannot be saved back into the script, so the next time the script runs, the dialog will appear again. Because of this you may want to think twice before saving a script as run-only. You’re probably saving it this way because you want to keep it from prying eyes—you want to be able to send the script to other people so they can use it, without their being able to read it. But you should weigh against this the possibility that you will anger your users when the script doesn’t work properly and they can’t do anything about it.

Warning

In my tests, it was possible at least sometimes to open a run-only script within the Script Editor, though of course no text appeared. It was then possible to press the Run button, but nothing would happen. This entire situation is very confusing, and I regard it as a bug (presumably in Script Editor); it should not be possible to open a run-only script in an editor at all.



[3] But this does not “dirty” the script file itself, so you can then close it, and it will not ask whether you want to save it, and will not retain this knowledge of the application’s new location; you’ll have to go through the whole rigmarole again the next time you open the script file. I regard this as the fault of AppleScript, which apparently has no way to alert the Script Editor that the compiled script has changed.

Get AppleScript: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.