Ada-ASSURED/Ada-Utilities Scripting Language Tutorial


Contents

Summary

Ada-ASSURED's scripting language is an extension of the Scheme programming language. It can be used for customization, integration, code analysis, and code transformation. This tutorial describes how to write such scripts. You do not need to know Scheme beforehand because footnotes describe each Scheme feature as it is introduced.

Ada-ASSURED and its scripting language are bundled into Ada-Utilities, an enhanced toolset derived from Ada-ASSURED that adds batch processing and Ada-Audit, a code auditor for producing aggregate reports about multiple files. See Ada-ASSURED vs. Ada-Utilities for further explanation of the contrasting application of the scripting language in these two products.

The tutorial describes features of Ada-ASSURED supported in Release 3.1.2.

Interactive and Batch Scripts

Ada-ASSURED is an interactive editor, library browser, standards enforcer, and quality assurance tool. The scripting language is used to extend it with customized commands that perform additional language-sensitive analyses and manipulations during interactive use.

Ada-ASSURED, when used in conjunction with Ada-Utilities, is also a batch standards checker, quality assurance tool, and code transformation system. The scripting language is used to implement analyses and manipulations of Ada code to be executed by Ada-ASSURED's Batch Processing Module.

The power of Ada-ASSURED's scripting language comes from its structured internal representation of your code. Ada-ASSURED automatically categorizes each construct in your file. In contrast, if you use text-oriented tools like awk, sed, grep, or perl, you must do the categorization yourself. As a result, tasks performed with difficulty using a text-oriented tool are often trivial in Ada-ASSURED.

A simple example illustrates the difference. To count the number of assignment statements in a file, you can't just count how many times ":=" occurs in the file. For example, procedure p contains only one assignment statement, not three:

    procedure p(a: in integer := 0) is
  begin -- p
     a := 1; -- Only one assignment on this line! :=(
  end p;

A text-oriented script must distinguish among occurrences of ":=" that are parameter initializers, assignment statements, and commentary. In contrast, Ada-ASSURED has already done this categorization for you. The script expression

  (sg:traverse (sg:buffer-term)
     ( (|StatementAssignment| _ _)(increment-count)))

traverses the entire file and calls function increment-count once for each assignment statement. It couldn't be simpler.

Scripts can easily be packaged for both interactive and batch use. As a consequence, programmers writing and modifying code can have interactive access to the same diagnostics the QA team has in batch. Programmers can bring code into compliance before QA and avoid costly rework cycles.

Ada-ASSURED used in interactive mode makes a powerful development environment for authoring batch analysis tools. Scripts can be edited in one buffer while an Ada test file is edited in another buffer. Scripts can be written and tested incrementally using the built-in Scheme interpreter.

Notation

The following font conventions are adopted:

    • Descriptive text is written in Times Roman.
    • Commands are written in Times Boldface, e.g., eval-string.
    • Syntactic categories in the scripting language are written in Times Italic, e.g., expressions.
    • Menu, buffer, and file names are written in Times Roman Brown, e.g., Tools/Scripts/Eval String.
    • Key names are written in Times Roman Caps, e.g., RETURN.
    • Operator and phylum names are written in Times Roman Green, e.g., Sum.
    • Programmatic text, whether it be Ada or Scheme, is written in Courier, e.g., (sg:write-message "Hello World").

Additional Documentation

The volume Ada-ASSURED Scripting Language: Supplemental Manual, which is distributed with Ada-ASSURED, contains:

    • The Revised4 Report on the Algorithmic Language Scheme. This is the defining document of the Scheme programming language.
    • The STk Reference Manual. This is the defining document of the STk implementation of Scheme used by Ada-ASSURED. It describes STk's extensions to Scheme, as well as features of standard Scheme that are implemented differently or not at all in STk.

The Ada-ASSURED User Guide and Reference Manual, which is distributed with Ada-ASSURED, contains:

    • Chapter 15: Scripting Language. This is the defining document for the Ada-ASSURED scripting language. It assumes a generic understanding of Scheme. All functions with names of the form sg:x are specific to Ada-ASSURED and are described in this chapter.

Ada-ASSURED's online documentation subsystem contains the following hypertext material:

    • The Revised4Report on the Algorithmic Language Scheme. This is a hypertext version of the manual provided in Ada-ASSURED Scripting Language: Supplemental Manual.
    • Scheme function prototypes. A list of function prototypes for all Scheme functions specific to Ada-ASSURED.
    • Ada grammar. Ada-ASSURED represents Ada code in structured form as a derivation tree with respect to this grammar. Scripts can navigate Ada code according to that structure.

Good books about Scheme include:

    • Scheme and the Art of Programming, George Springer and Daniel P. Friedman, The MIT Press and McGraw-Hill Book Company, 1989.
    • Structure and Interpretation of Computer Programs, Second Edition, Harold Abelson and Gerald Jay Sussman, with Julie Sussman, The MIT Press and McGraw-Hill Book Company, 1996.

Hello World

We start with the standard "Hello World" example. Function sg:write-message prints a message in the status pane of an Ada-ASSURED window[1]. For example, executing the script[2]

  (sg:write-message "Hello World")

displays the text "Hello World".

A simple way to enter and execute a short script is to use the eval-string command (menu: Tools/Scripts/Eval String). Invoking eval-string pops up a dialog box in which the script can be entered. After typing the script, clicking OK or striking RETURN in the dialog box triggers script execution.

Commands and Functions

A user seated at a workstation interacts with Ada-ASSURED by invoking commands. There are numerous ways to invoke commands: typing a name in the command pane, selecting a menu item, striking a key, clicking a mouse button, etc.

Every command corresponds to a function in the scripting language. For example, command save corresponds to function sg:save. In general, each command c corresponds to function sg:c. Thus, any action that a user can manually perform can also be invoked in a script by a function call.

Invoking a command with no parameters calls the corresponding scripting function with no arguments. For example, invoking command save does exactly the same thing as using command eval-string to execute the script[3]

  (sg:save)

If a command has parameters, then invoking it pops up a dialog box in which the needed arguments are specified. Clicking OK or striking RETURN in the dialog box calls the corresponding scripting function with the arguments that have been specified. For example, suppose command save-as is invoked and parameters "Text", "foo", and "BASEVIEW" are entered in the appropriate fields of the dialog box. Then clicking OK does exactly the same thing as using command eval-string to execute the script[4]

  (sg:save-as "Text" "foo" "BASEVIEW")

Commands and scripting functions are so similar that you may wonder how they differ. The few differences are:

    • A command with arguments has dialog boxes for the preparation of arguments, whereas the corresponding scripting function is called with arguments directly.
    • Menu items, keystrokes, and mouse actions invoke commands.
    • A sequence of commands can be captured as the definition of an editor macro.
    • Commands are listed by describe-command and are expected to have a corresponding file in the hypertext documentation.

Another point of possible confusion concerns the name of a command, e.g., save-as, and the textual label for the command in a menu, e.g., Save As. In general, the two are not the same. To learn the name of the command associated with a given menu item, use command describe-menus (menu: Help/Describe Menus).

Evaluating Scripts Interactively

Buffer *transcript* provides a convenient interface to the scripting language interpreter. When Ada-ASSURED is used in interactive mode, the *transcript* buffer is effectively the standard input, standard output, and standard error device of the script interpreter.

Use command switch-to-buffer (menu: View/Buffers/Switch to Buffer) to create a separate window displaying buffer *transcript*. The buffer may already contain output from previous evaluations using command eval-string. If it does, just erase it.

If you type a Scheme expression in the *transcript* window and strike RETURN, the expression is evaluated and its value is printed on the next line. For example, typing the expression[5]

  (+ 2 (* 3 4))

and striking RETURN causes the output 14 to be printed on the next line.

A runtime error in a script causes the script to terminate with an error message that is printed in buffer *transcript*. For example, entering the type-incorrect expression[6]

  (- "Hello" "World")

and striking RETURN causes the following error message to appear in *transcript*:

  (- "Hello" "World")
  *** Error:
  -: not a number: "Hello"
  Stack trace:
    0: (- Hello World)

    1: (sg:send-input)

The stack trace shows the nesting of function calls within which the error occurred. Function sg:send-input is the function that is invoked when you strike RETURN; it submits the line of script text to the Scheme interpreter for evaluation. All script errors are reported in buffer *transcript* regardless of how the script was started.

Striking RETURN in buffer *transcript* evaluates whatever text appears on the current line. Thus, a previously entered expression can be re-edited and re-evaluated without retyping it. Just select the line containing the expression, re-edit as desired, and strike RETURN.

A very common error in Scheme is to mismatch parentheses. Here is an example that illustrates what to expect if you leave off a right parenthesis:

  (+ 2 (* 3 4)
  *** Error:
  end of file inside list
  Stack trace:
    0: (sg:send-input)

The "end of file" referred to in the error message is the end of the *transcript* buffer. Here is an example that illustrates what to expect if you have an extra right parenthesis:

  (+ 2 (* 3 4)))
  *** Error:
  unexpected close parenthesis
  Stack trace:
    0: (sg:send-input)

The *transcript* buffer is designed for convenient entry and evaluation of single-line expressions. Suppose you want to enter and evaluate an expression that spans several lines. Striking RETURN after entering the first line would cause an error because the first line, by itself, is not syntactically correct. Instead, you can terminate the first line, and each subsequent line, with LINEFEED (^J). Striking RETURN at the end of the last line would also cause an error because the last line, by itself, is not syntactically correct. The entire expression can be evaluated by selecting it, i.e., dragging over it with the mouse, and invoking command eval-selection (menu: Tools/Scripts/Eval Selection).

Varieties of Output

The scripting language supports numerous forms of output, as we now illustrate.

Our first "Hello World" program used function sg:write-message to output the message to the status pane of the current window. When a script is initiated by eval-string, the current window is the window from which eval-string was invoked. This is useful for providing status information and short answers to user queries in interactive mode. In batch mode, messages written by sg:write-message in scripts are directed to the UNIX stderr stream.

A second "Hello World" program outputs the message to Scheme's standard output port:

  (display "Hello World\n")

In interactive mode, the output is appended to buffer *transcript*. In batch mode, the output is appended to the UNIX stdout stream.

A third "Hello World" program always outputs its message to a specific file regardless of whether Ada-ASSURED is run in batch or interactive mode. Executing the script[7]

  (define fd (open-output-file "/tmp/world"))
  (display "Hello World" fd)
  (close-output-port fd)

opens output file /tmp/world, writes the message to that file, and closes the file. This is just standard Scheme code.

A fourth "Hello World" program invokes a shell to output the message. Executing the script[8]

  (system "echo 'Hello World'")

outputs its message to the UNIX stdout stream. Using this mechanism, a script can invoke any other program by an appropriate shell command line.

Defining Commands With No Parameters

Defining a new command that has no parameters can involve the following steps:

      1. Defining a Scheme function that implements the command's action.
      2. Defining the command itself.
      3. Defining a menu item for selecting the command (optional).
      4. Defining a key binding for selecting the command (optional).
      5. Writing hypertext documentation for the command (optional).

We illustrate by defining a new command that meets the following specification:

Command analyze prints the message "Buffer: f ", where f is the name of the buffer in which the command is invoked. Menu item Tools/Analyze selects command analyze.

The command is purposely vacuous so we can concentrate on how commands are defined in general, not on the details of a specific command.

Step 1. Scheme function analyze is defined by evaluating the following script:[9]

  (define (analyze)
    (sg:write-message
     (string-append
      "Buffer: "
      (sg:current-buffer) )))

The current buffer is part of the global state of Ada-ASSURED, and is thus an implicit parameter of analyze. The value of expression (sg:current-buffer) is the name of the current buffer. The buffer name is appended to the string "Buffer: ", which is then output.

It is convenient, but not mandatory, for the function that implements command c and the command itself to have related names. It is also good practice to uniquely prefix all global names to prevent clobbering a predefined name. However, we shall not follow that practice here.

Step 2. Command analyze is added to Ada-ASSURED by evaluating the following script: [10]

  (sg:add-command "analyze" analyze)

The first argument of sg:add-command is the name of the command as a string, and the second argument is the function that implements the command.

Step 3. Command analyze is added to menu Tools by evaluating the following script:

  (sg:add-menu "Tools" "Analyze" 99 #\A #\b '() "analyze")

where the arguments have the following significance:

    • "Tools" is the menu into which the menu item is inserted.
    • "Analyze" is the textual label of the menu item.
    • 99 indicates the new item should follow the 99th item --- presumably last.
    • #\A is a mnemonic for the item.[11]
    • #\b indicates the item is a button (as opposed to, say, a descending menu).
    • '() means the menu item is always enabled, i.e., it is never shaded out.
    • "analyze" is the name of the command invoked by selecting the menu item, i.e., analyze.

It is instructive to consider the distinct functionality enabled by each of the above steps.

After Step 1, you can use command eval-string to evaluate Scheme expression

  (analyze)

to perform the appropriate operation. Although the semantics of the command are defined by function analyze, the command itself does not yet exist.

After Step 2, you can enter command name analyze in the command pane and strike RETURN to invoke function analyze. Also, analyze is listed by command describe-command, although selecting it will fail to find appropriate documentation.

After Step 3, you can select menu item Tools/Analyze to invoke command analyze.

An important rule to follow when testing a script is: be sure to invoke it from an appropriate window. For example, command analyze works on the current buffer. If you invoke it from the window containing the script itself, e.g., *transcript*, the script will be analyzed, not an Ada program!

Script Files

The previous section presented three script fragments that, when evaluated, define function analyze, define command analyze, and add item Analyze to the Tools menu. The question of how these fragments were entered and evaluated was left open. For illustrative purposes, they could have been entered interactively using either command eval-string or the *transcript* window. In contrast, a script that is intended for repeated use is typically developed in a buffer and saved in a file. This section describes this typical scenario for developing a script:

Step 1. Create a new buffer for the script using command new (menu: File/New). The buffer name (and the file name) should have an stk suffix, e.g., custom.stk. The syntactic category of the buffer should be TEXT because the file will contain ASCII text, not Ada code.[12]

Step 2. Type the script using normal text-editing commands. For example, enter the three fragments given previously:

  (define (analyze)
    (sg:write-message
     (string-append
      "Buffer: "
      (sg:current-buffer) )))
  (sg:add-command "analyze" analyze)
  (sg:add-menu "Tools" "Analyze" 99 #\A #\b '() "analyze")

Step 3. Test the script using command eval-buffer (menu: Tools/Scripts/Eval Buffer), which evaluates the entire buffer. Evaluating the script defines function analyze, defines command analyze, and adds menu item Analyze to the Tools menu. Note that evaluating the script does not call function analyze, and does not invoke command analyze. To test the command, you must invoke it, say by selecting it from the Tools menu.

Step 4. If a script requires modification, reedit the buffer and retest as described in Step 3. If you use command eval-buffer to run the entire script again after modification, the old definitions of function analyze, command analyze, and menu item Analyze will be overwritten with new definitions.[13] For some scripts, repeated evaluation is undesirable. If you want to reevaluate part of the buffer, select the part by dragging over it with the left mouse button (SELECT) and invoke command eval-selection (menu: Tools/Scripts/Eval Selection).

Step 5. Once the script is debugged, save the buffer using command save-as (menu: File/Save As. The buffer can be saved using command save (menu: File/Save) after it has been saved once.

Now suppose you start up a fresh copy of Ada-ASSURED and want to enable analyze. You can use command eval-file (menu: Tools/Scripts/Eval File) to evaluate script file custom.stk. However, you would have to remember to do this on every startup. Alternatively, you can arrange for the file to be evaluated automatically on every Ada-ASSURED startup. Section 15.3 of the Ada-ASSURED User Guide and Reference Manual describes the different script files that are evaluated on startup. Placing the script

  (load "custom.stk")

in one of these files causes file custom.stk to be evaluated. Exactly which file should contain the load depends on whether the command is being added for an entire site, a project, or an individual. Alternatively, scripts can be written directly in the given files.

To modify the script in file custom.stk, use command open (menu: File/Open) and proceed from Step 4.

Defining Commands With Parameters

This section shows how to define commands with parameters. We illustrate by redefining command analyze so that it has one parameter. The new specification is:

Command analyze prints the message "Buffer: f m ", where f is the name of the buffer in which the command is invoked, and m is a message that is entered as a parameter in a dialog box. Menu item Tools/Analyze selects command analyze.

We begin by adding a parameter m to function analyze, which ultimately performs the command action:

  (define (analyze m)
    (sg:write-message
     (string-append
      "Buffer: "
      (sg:current-buffer)
      " "
      m )))

Next, we want command analyze to pop up a dialog box with a slot for parameter m, so we change command analyze to call function analyze-dialog:

  (define (analyze-dialog)
    (sg:make-dialog "analyze" "Enter Message" "Text:"))
  (sg:add-command "analyze" analyze-dialog)

The arguments of sg:make-dialog have the following significance:

    • "analyze" is the name of the procedure to be called when OK is clicked. Whatever text appears in the dialog box parameter slot at the time OK is clicked is passed as an argument to analyze.
    • "Enter Message" is a title for the dialog box.
    • "Text:" is a label for the parameter slot.

There is no change in the way the menu item is created, i.e., "analyze" is the name of the command invoked when the menu item is selected, not the name of the function called:

  (sg:add-menu "Tools" "Analyze" 99 #\A #\b '() "analyze")

Structural Representation of Ada Code

When you use command new to create a new buffer, or when you use command open to read a file into a buffer, you specify a syntactic category. A buffer of syntactic category TEXT can be used to edit any text file. In contrast, a buffer of syntactic category compilation can only be used to edit Ada code.

The syntactic category of a buffer determines how it is represented internally, which in turn determines how it can be accessed in the scripting language. A TEXT buffer is essentially just a formatted string of characters. In contrast, a compilation buffer contains a tree-structured representation of Ada code.

Consider the assignment statement:

  J := J + 1;

Viewed textually, it is just a sequence of characters. Viewed abstractly, an assignment statement consists of a name and an expression. In the example, the name is J and the expression is J+1. The abstract structure can be represented by the tree fragment:

Node label StatementAssignment identifies the construct as an assignment statement. The two children of this node are the left and right sides of the assignment statement. In the abstract tree representation of the statement, there is no need for := or semicolon tokens because the node label and the structure of the tree provide sufficient information.

A window that displays a compilation buffer has a dual representation and can be manipulated according to either its textual or its structural representation. Thus, although the text of the assignment statement J:=J+1; appears in the window, the tree structure is maintained under the hood.

The rules for constructing the structural representation of Ada code are considered a grammar. For example, the grammar rule for assignment statements is:

  statement: StatementAssignment( name expression )

which can be read as:

One kind of statement is called a StatementAssignment and consists of two parts, a name and an expression.

The colon and the parentheses that appear in the rule are part of the notation used to present grammar rules and should not be confused with characters that actually appear in the textual representation of Ada code. In contrast to the rules of the Ada LRM, the grammar rules considered here describe the abstract structure of Ada and say nothing about its textual representation.

Here are some more rules of the Ada grammar:

    expression:
              Sum( expression expression )
          |  ExpressionName( name )
          |  ExpressionNumber( number )

which can be paraphrased as:

    One kind of expression is called a Sum and consists of two parts; each part is an expression.
    Another kind of expression is called an ExpressionName and consists of one part that is a name.
    Another kind of expression is called an ExpressionNumber and consists of one part that is a number.

Using these rules, we can refine our picture of the structural representation of the assignment statement:

In the abstract tree representation of expressionJ+1, node label Sum categorizes the expression and the + token is omitted. We write jay and one as placeholders for the representation of J as a name and 1 as a number. Because grammar rules have not been presented for name and number, jay and one are not further defined here.

Another way to write the structural representation of the assignment statement is:

    StatementAssignment(jay, Sum(ExpressionName(jay), ExpressionNumber(one)))

In this presentation, you can think of node labels as constructor functions that are applied to arguments to produce the tree structure. From this perspective, the grammar rule

    statement: StatementAssignment( name expression )

can be read as a specification of the constructor function StatementAssignment. It states:

The constructor function StatementAssignment requires two arguments. The first argument must be a name and the second argument must be expression. The result of applying StatementAssignment to its arguments is a statement.

Thus, the constructor function StatementAssignment has typed arguments and produces a typed result. Ada-ASSURED refuses to build any tree structure that does not satisfy the type requirements of the constructor functions. For example, a statement can not appear on the right side of an assignment statement because the second argument of StatementAssignment must be an expression.

When the assignment statement is structurally selected in a compilation buffer, it is underlined:

  J := J + 1;

Think of the structural selection as designating a node in the tree representation of the buffer, e.g., the StatementAssignment node. Similarly, when the right side of the assignment is structurally selected, it is underlined:

  J := J + 1;

corresponding to the Sum node in the tree.

We have been using informal and intuitive terminology. To have an unambiguous way to talk about the structure of Ada code, we adopt the following technical terminology:

informal terminology

technical terminology

tree

term

constructor function, node label, e.g., StatementAssignment

operator

number of arguments required by a constructor function

arity of an operator

syntactic category, type, e.g., statement

phylum

Using this technical terminology, we may say:

Operator StatementAssignment has arity 2. Its first argument must be a term of phylum name and its second argument must be a term of phylum expression. The result of applying operator StatementAssignment to its arguments is a term of phylum statement.

Operator, phylum, and term are abstract data types of the scripting language. The following example illustrates some of the functions of these data types:

  (define assignment (sg:string->operator "StatementAssignment"))
  (define assignment-arity (sg:operator-arity assignment))
  (define name (sg:string->phylum "name"))
  (define expression (sg:string->phylum "expression"))
  (define assignment-template
    (sg:build assignment
      (sg:placeholder-term name)
      (sg:placeholder-term expression)))

After executing this script, assignment is operator StatementAssignment; assignment-arity is 2, name is phylum name, expression is phylum expression, and assignment-template is the term that is textually displayed as:

  <name> := <expression>;

The details of these functions are not important here. They are presented just to illustrate what sort of operations are available on the abstract data types.

The technical terminology introduced here is used in the rest of the tutorial, as well as in the Ada-ASSURED User Guide & Reference Manual. The complete grammar used by Ada-ASSURED to categorize Ada code is given in an on-line hypertext document and is also printed in the Ada-ASSURED Batch Processing Module documentation.

Structurally Traversing a Buffer

The previous section explained how Ada code is represented structurally in a buffer of syntactic category compilation. By navigating over this structure, scripts can easily

    • search for Ada code fragments of particular interest,
    • transform Ada code fragments of particular interest, or
    • compute properties of the Ada code.

Because the manipulations are performed on the structure of the code, the scripts do not depend on incidental details of textual layout.

The contents of the current buffer, represented as a term, is given by evaluating script expression (sg:buffer-term). You can analyze the contents of the buffer by computing on this term. The structure of the term gives you the structure of the compilation unit on which to navigate.

A common way to compute on a term is to traverse it top-down left-to-right. This is known as a preorder traversal of the term. The nodes of the following term are labeled in preorder:

The script form

  (sg:traverse term(pattern body) ... )

performs a preorder traversal of the given term. The term argument is followed by one or more (pattern body) clauses. At every node reached during the preorder traversal, each pattern is considered in turn. The first pattern to match a node has its body executed, after which the traversal continues. All patterns may fail to match a given node, in which case the traversal just continues to the next node in preorder. Conceivably, no pattern matches during an entire traversal.

The simplest pattern is the wildcard pattern that matches every node. The wildcard pattern is written with an underscore character. Thus, the expression

  (sg:traverse term( _ body) )

executes body at each and every node of term. For example, suppose we want to know how many nodes a term contains. We can initialize a local variable count to 0, and then use sg:traverse with the wildcard pattern to increment count at each node. When the traversal is finished, count will contain the number of nodes:[14]

  (let ((count 0))
    (sg:traverse
term( _ (set! count (+ count 1))))
    count)

We can use this expression as the body of function count-nodes, which takes the term to be analyzed as a parameter:[15]

  ;;; Return the number of nodes in t.
  (define (count-nodes t)
    (let ((count 0))
      (sg:traverse t ( _ (set! count (+ count 1))))
      count))

Finally, function analyze can be redefined to be less vacuous: [16]

  (define (analyze)
    (sg:write-message
      (number->string (count-nodes (sg:buffer-term))) ))
  (sg:add-command "analyze" analyze)

Note that function analyze is only meaningful in buffers of syntactic category compilation. If analyze is invoked in a TEXT buffer, it will return 0.

Admittedly, the number of nodes in the tree representation of a buffer is still not particularly useful. The next section introduces patterns that permit more interesting metrics to be defined.

Simple Patterns

If o is an operator of arity n, then (o pattern1 … patternn) is a pattern. For example, |StatementAssignment| is an operator of arity 2. Accordingly,

  (|StatementAssignment| _ _)

is a legal pattern.

Each assignment statement in the term representation of an Ada compilation unit is an instance of the |StatementAssignment| operator. During a traversal, pattern (|StatementAssignment| _ _) only matches assignment statement nodes. We can use this pattern in a modification of function count-nodes that only counts assignment statements:

  ;;; Return the number of assignment statements in t.
  (define (count-assignments t)
    (let ((count 0))
      (sg:traverse t
        ( (|StatementAssignment| _ _)
          (set! count (+ count 1)) ))
      count))

When pattern (|StatementAssignment| _ _) matches a node, the wildcard patterns match the left and right sides of the assignment. We use wildcard pattern for these components because we do not need to refer to them in the corresponding body.

Now suppose that, instead of just counting assignment statements, we wish to compute the average complexity of their right hand sides. For simplicity, we will use the number of nodes in the right hand side as a measure of its complexity. Each time pattern

  (|StatementAssignment| _ rhs)

matches a node, pattern variable rhs is bound to the expression on the right hand side of the assignment statement. We can pass rhs to function count-nodes to determine its complexity:

  ;;; Return the average complexity of assignment statements in t.
  (define (average-assignment-complexity t)
    (let ((count 0) (complexity 0))
      (sg:traverse t
        ( (|StatementAssignment| _ rhs)
          (set! count (+ count 1))
          (set! complexity
            (+ complexity (count-nodes rhs))) ))
      (/ complexity count) ))

Function sg:traverse just traverses the term it is given as an argument. For example, when count-nodes is called on rhs, its traversal is restricted to the subterm rhs. Unless directed to do otherwise, function sg:traverse visits every node in the term, including children within a node that has matched. For example, in function average-assignment-complexity, after updating the statistics for a given assignment statement, the traversal resumes within the StatementAssignment subterm. Clearly, no assignment statements will be found with the name or expression parts of an StatementAssignment. The traversal of the matched subterm can be omitted if efficiency is important. Suppose a (pattern body) matches. Then if the value of the last expression in body is the symbol 'skip, the traversal of the matched subterm is omitted, as illustrated in the following alternate definition of function average-assignment-complexity:[17]

  ;;; Return the average complexity of assignment statements in t.
    (define (average-assignment-complexity t)
      (let ((count 0) (complexity 0))
        (sg:traverse t
          ( (|StatementAssignment| _ rhs)
              (set! count (+ count 1))
              (set! complexity (+ complexity (count-nodes rhs)))
              'skip))
        (/ complexity count) ))

Note that the correctness of the optimized function relies on our analysis that neither name nor expression can contain assignment statements. Because incorrect analyses lead to errors, such optimizations are probably not worthwhile unless the time saved in omitting the traversal is substantial. For example, if we are counting procedure definitions, it may well be worth omitting the traversal of the statements in the body of the procedure.

Nested Patterns

Patterns can contain other patterns to an arbitrary level of nesting. The pattern

  ( |StatementAssignment| _ _)

illustrates nested patterns, albeit only nested wildcard patterns. The nested pattern

  ( |StatementAssignment| _ (|Sum| _ _))

only matches assignment statements whose right hand sides are sums. The nested pattern

  ( |StatementAssignment| n (|Sum| (|ExpressionName| n) _))

only matches assignment statements that have the form

  n := n + expression ;

where n is some (arbitrarily complicated) name. When a pattern contains a repeated pattern variable like n, it only matches if all repetitions match equal subterms. Pattern matching is performed on the abstract term representation of the program and has nothing whatsoever to do with how the subterm are formatted as text. For example, n could be a compound name whose different occurrences are split across lines differently.

When you write a pattern, be sure it is "type correct". For example, the pattern

  ( |StatementAssignment| n (|Sum| n _))

will never match because the first operand of Sum is an expression, whereas the left side of an assignment statement is a name.

Here is a function that illustrates nested patterns. It also illustrates a traversal that has more than one (pattern body) pair. Note that the scope of a pattern variable is limited to the pattern in which it appears. Thus, there of two distinct pattern variables named n in the example.

  ;;; Count assignment statements in t that have one of the two forms
  ;;; n := n + expression; or n := expression + n;
    (define (count-increments t)
      (let ((count 0) )
        (sg:traverse t
          ((|StatementAssignment| n (|Sum| (|ExpressionName| n) _))
              (set! count (+ count 1)) )
          ((|StatementAssignment| n (|Sum| _ (|ExpressionName| n)))
              (set! count (+ count 1)) ))
        count))

Traversing the Structural Selection

We have seen that the contents of the current buffer, represented as a term, is given by evaluating script expression (sg:buffer-term). The examples given so far analyze entire buffers. In this section, we see how to analyze an arbitrary Ada fragment.

Suppose function analyze has been written to take an arbitrary term as argument. For example, suppose analyze just calls the function count-increments defined in the previous section:

  ;;;; Analyze term t and return a number.
  (define (analyze t) (count-increments t))

and function analyze-buffer calls analyze with the term representing an entire buffer:

  ;;; Analyze the current buffer and output result.
  (define (analyze-buffer)
    (sg:write-message
      (number->string (analyze (sg:buffer-term))) ))
  ;;; Create command analyze-buffer.
  (sg:add-command "analyze-buffer" analyze-buffer)

We can use function analyze to define a second command that performs the same analysis, but just on the currently selected subterm of the buffer. The value of expression (sg:structural-selection-apex) is the selected subterm of the current buffer.

  ;;; Analyze the current structural selection and output result.
  (define (analyze-selection)
    (sg:write-message
      (number->string
        (analyze (sg:structural-selection-apex))) ))
  ;;; Create command analyze-selection.
  (sg:add-command "analyze-selection" analyze-selection)

To analyze a fragment of the buffer, e.g., the expression on the right side of an assignment statement, you structurally select it and invoke command analyze-selection.

Although the above code is essentially correct, there is a problem, as we now explain. The problem arises in syntactic contexts where a list of items is permitted. For example, consider the following Ada code fragment in which two of four statements are selected:

  if I = 0 then
    J := 1;
    K := 2;
    L := 3;
    M := 4;
  end if;

To understand why command analyze-selection fails in this case, and how to correct it, you must understand two things:

    • The representation of lists as terms (e.g., the list of four statements above).
    • The representation of sublist selections (e.g., the selected sublist of two statements above).

Operators in the Ada grammar have fixed arity but, between then and else, an unbounded list of statements is permitted. How can such an unbounded list of statements be represented using only operators of fixed arity?

The answer is that the arity 2 operator statement_listCons is used to pair each statement with the list of statements that follows it. The arity 0 operator statement_listNil is used to represent the list terminator, an empty list of statements. The result is a term that looks like this:

The relevant portion of the Ada grammar is:

  statement_list:
      statement_listNil( )
    |  statement_listCons( statement_choice statement_list )

which states that a statement_list consists of 0 or more statement_choice's. Phylum statement_choice is used to represent a statement preceded with an optional label and followed by an optional trailing comment.

The representation of a list of statements is analogous to Scheme's standard representation of lists[18]. For example, Scheme list (1 2 3 4), when depicted as a tree, has exactly the same shape as the sample list of statements

Given our representation of a list of statements, how do we represent the selected sublist in

  J := 1;
  K := 2;
  L := 3;
  M := 4;

The following figure shades the selected sublist:

Clearly, no one subterm represents this selection. We can now understand the flaw in command analyze-selection, as defined above. The value of expression (sg:structural-selection-apex) is the subterm shaded below:

In other words, the apex of the structural selection is the sublist of statements beginning at the first selected statement, extending all the way to the end of the list. If function analyze counted assignment statements, it would have returned 3, not 2.

In general, a selection is a list of two terms, (apex exclusion). The apex is where the sublist begins and the exclusion is where it ends. Think of a sublist selection (apex exclusion) as the apex minus the exclusion. The exclusion, in our example, is the subterm shaded below:

Selections only have the form (apex exclusion) when a sublist is selected. When an entire term is selected, the selection has no exclusion, i.e., the selection has the form (apex).

We can now correct the previous definition of analyze-selection:

  ;;; Analyze the current structural selection and output result.
  (define (analyze-selection)
    (sg:write-message
      (number->string
        (analyze (sg:get-structural-selection))) ))
  ;;; Create command analyze-selection.
  (sg:add-command "analyze-selection" analyze-selection)

Function analyze must now be prepared for an argument that is either a term or a selection:

  ;;; Analyze term or selection s and return a number.
  (define (analyze s)
<some analysis of t> )

To accommodate this situation, function sg:traverse is actually more general than we have previously indicated. It handles either a term or a selection in its first argument:

  (sg:traverse term(pattern body) ... )
  (sg:traverse
selection(pattern body) ... )

In general, all Ada-ASSURED script functions that take a selection argument also take a single term as well. Therefore, the following version of analyze-selection counts assignment statements in s regardless of whether it is a selection or just a term:

  ;;; Return the number of assignments in term or selection s.
  (define (analyze-selection s)
    (let ((count 0))
      (sg:traverse s
        ( (|StatementAssignment| _ _)
          (set! count (+ count 1)) ))
      count))

Simple Metrics

The previous sections illustrated tiny scripts that count various forms of assignment statements. In this section, we present a slightly more realistic example with the following specification:

Command bean counts occurrences of various Ada constructs ("beans") in the current buffer and displays the information using Ada-ASSURED's HTML documentation browser. Menu item Tools/Count Constructs selects command bean. Here is some sample output:

Sample Metrics Report for foo.a

File: foo.a
Generated: 3/5/97 by joe_user

Item

Count

package-specifications

3

procedure-specifications

5

type-declarations

11

names

106

comments

20

pragmas

0

statements

56

gotos

1

The concepts illustrated by the example include:

    • Pattern matching for multiple constructs during a single tree traversal.
    • Use of hash tables in Scheme.
    • Use of files in Scheme.
    • Use of formatted output in Scheme.
    • Invocation of UNIX programs from within Scheme.
    • Access to UNIX environment variables from within Scheme.
    • Invocation of Ada-ASSURED's HTML browser on a file.

The script is included with Ada-ASSURED in $AAHOME/scripts/bean.stk.

Two global variables are used for the table of counts and the name of the generated file:

  ;;; Table for counts indexed by bean type.
  (define bean:table '())
  ;;; Name of generated HTML file.
  (define bean:html-file "bean.html")

Function bean:start implements command bean:[19]

  ;;; Generate bean count data in file bean:html-file.
  ;;; Then invoke the HTML browser on the file.
  (define (bean:start)
    ;;; Set bean:table to an empty hash table indexed by strings.
      (set! bean:table (make-hash-table string=?))
    ;;; Increment bean counts for the current buffer.
      (bean:traverse)
    ;;; Create the HTML file.
      (bean:output-html)
    ;;; Invoke the HTML browser on the generated file.
      (sg:load-html-file (getcwd) bean:html-file) )

Once function bean:start has been defined, we can define command bean and add an item for it to the Tools menu:

  ;;; Define command bean.
  (sg:add-command "bean" bean:start)
  ;;; Create menu item for command bean.
  (sg:add-menu "Tools" "Count Constructs" 99 #\o #\b '() "bean")

Function bean:count is used to increment the counter associated with a given bean type. It must distinguish between the first occurrence of a construct (in which case the count is initialized to 1) and subsequent occurrences of the construct (in which case the count is incremented).[20]

  ;;; Increment count for bean type s, where s is a string.
  (define (bean:count s)
    (let ((hte (hash-table-get bean:table s '())))
      (if (integer? hte)
        (hash-table-put! bean:table s (+ 1 hte))
        (hash-table-put! bean:table s 1) )))

Function bean:traverse increments the appropriate count for each occurrence of one of the constructs of interest. For exposition purposes, we have used pattern variables below to indicate the contents of various operator arguments. Because these pattern variables are not used, however, they could have been wildcards instead.

  ;;; Traverse buffer and increment appropriate count
  ;;; for each occurrence of a bean.
    (define (bean:traverse)
      (sg:traverse (sg:buffer-term)
        ((|PackageSpec| name decl_list opt_private)
          (bean:count "package-specifications"))
        ((|SubprogramSpecificationProcedure| name params)
          (bean:count "procedure-specifications"))
        ((|TypeDeclaration| t _)
          (bean:count "type-declaration"))
        ((|NameIdentifier| id)
          (bean:count "names"))
        ((|LeftCommentLine| text)
          (bean:count "comments"))
        ((|RightCommentLine| text)
          (bean:count "comments"))
        ((|Pragma| id _ _)
          (bean:count "pragmas"))
        ((|Statement| label statement comment)
          (bean:count "statements"))
        ((|StatementGoto| name)
          (bean:count "gotos") )))

Once the buffer has been traversed and all counts have been incremented, the HTML file is output. We assume you have a passable reading knowledge of HTML, and will not explain the HTML here:[21]

  ;;; Output HTML table for bean counts.
  (define (bean:output-html)
    (let ((port (open-output-file bean:html-file)))
      ;;; Output the header of the HTML file.
        (bean:html-header port (sg:current-buffer))
      ;;; Output the beginning of the HTML table.
        (format port
          "<TABLE BORDER>~%<TH>Item</TH><TH>Count</TH><TR>~%")
      ;;; Output one line of the HTML table per bean type.
        (hash-table-map bean:table
          (lambda (s c) (bean:html-counts port s c)))
      ;;; Output the end of the HTML table.
        (format port "</TABLE>~%")
      ;;; Output the trailer of the HTML file.
        (bean:html-finish port)
      ;;; Close the output file.
        (close-output-port port) )

  ;;; Output a line of the HTML table for bean type s with count c.
  (define (bean:html-counts port s c)
    (format port "<TD>~a</TD><TD ALIGN=CENTER>~a</TD><TR>~%" s c))

The header and trailer of the HTML file are pretty standard. The details have more to do with HTML than Scheme or Ada-ASSURED:[22]

  ;;; Output the header of the HTML file for bufname to port.
  (define (bean:html-header port bufname)
    (format port "<HTML>~%<HEAD>~%")
    (format port "<TITLE>~a</TITLE>~%" bufname)
    (format port "</HEAD>~%<BODY bgcolor=#ffffff>~%")
    (format port
      "<H1>Sample Metrics Report for ~a~%</H1>"
      bufname)
    (let ((s (sg:buffer-file (sg:current-buffer))))
      (if (string? s)
        (format port "File: <KBD>~a</KBD><P>~%" s)))
    (let ((s (exec "date")))
      (if (string? s)
        (format port "Generated: <KBD>~a</KBD> by ~a<P>~%"
          s (getenv "USER")))))
  ;;; Output the trailer of the HTML file.
  (define (bean:html-finish port)
    (format port "</BODY>")
    (format port "</HTML>~%") )

Conversion From Terms to Strings

Terms are abstract, non-textual representations of Ada code fragments. To display a term as readable text, the term must be converted to a string. The following example illustrates how this is done.

Suppose we want to print out the text of each assignment statement in a file. We have seen that it is easy to locate each assignment statement using sg:traverse:

  ;;; Print each assignment statement in term or selection s.
  (define (print-assignments s)
    (sg:traverse s
      ( (|StatementAssignment| _ _)
<print the matched statement> ) ))

Within the body of a (pattern body) pair, the name $$ refers to the matched subterm. Thus, to print each assignment statement, we must print term $$. However, if we were to write:

  ;;; Print each assignment statement in term or selection s.
  (define (print-assignments s)
    (sg:traverse s
      ( (|StatementAssignment| _ _) (display $$)) ))

the statements would be displayed in an opaque representation, e.g.,

  [Term 2c64ac]
  [Term 2d3240]
  
etc.

In order for the statements to be readable, we must convert each term to its string representation for display.

Function (sg:term->string term) converts term to a string using the standard formatting rules for Ada. Thus, the correct definition of function print-assignments is:

  ;;; Print each assignment statement in term or selection s.
  (define (print-assignments s)
    (sg:traverse s
      ( (|StatementAssignment| _ _)
          (display (sg:term->string $$)) )))

To try out print-assignments as a version of command analyze, function analyze should be redefined as follows:

  ;;; Print each assignment statement in structural selection.
  (define (analyze)
    (print-assignments (sg:get-structureal-selection)) )

Conversion From Strings to Terms

A string must be analyzed to obtain its representation as a term. The analysis process is called parsing. One string may have different representations as terms of different phyla. The following example illustrates how strings are converted to terms.

Suppose we wish to replace every assignment statement of the form:

  n := ABC + DEF * GHI - 1;

by the assignment statement

  n := f(ABC, DEF, GHI);

where n is an arbitrary name. We wish to find all assignment statements of the given form regardless of how they are formatted on the page, e.g., even if the statement is split across multiple lines.

The structural representation of expression ABC+DEF*GHI-1 is

  Difference( Sum( ExpressionName(abc),
         Product(ExpressionName(
def), ExpressionName(ghi))),
       ExpressionNumber(
one))

where abc, def, and ghi are the representations of the identifiers as names, and one is the representation of 1 as a number. Because this is tedious, we would prefer to avoid writing an explicit pattern for the expression. In fact, we can avoid any detailed understanding of the term representation of the expression, as shown below.

First, we obtain the term representations of expressionABC+DEF*GHI-1 and expressionf(ABC,DEF,GHI). Function (sg:string->term string phylum) converts a string to a term of a given phylum. If string is not syntactically correct as a phylum, the null list '() is returned. Function(sg:string->phylum string) returns the phylum named string. Thus, the term representations of the two expressions are computed by:

  (define expression-phylum
    (sg:string->phylum "expression") )
  (define exp1
    (sg:string->term "ABC+DEF*GHI-1" expression-phylum) )
  (define exp2
    (sg:string->term "f(ABC,DEF,GHI)" expression-phylum) )

To find all occurrences of assignment statements with the given form, we can use a hybrid between pattern matching and testing for term equality: [23]

  ;;; Replace each assignment statement in term or selection s
  ;;; with the form n:=ABC+DEF*GHI-1; by n:=f(ABC,DEF,GHI);
  (define (replace-assignments! s)
    (sg:traverse s
      ( (|StatementAssignment| _ rhs)
        (if (equal? rhs exp1)
          <
replace rhs with exp2> )
        'skip)))

Function (sg:replace! term1 term2) replaces term1 with term2. Function sg:replace! is the scripting language's assignment statement for subterms. Term1 is replaced in its context by term2. Thus, we can complete function replace-assignments! as follows:

  ;;; Replace each assignment statement in term or selection s
  ;;; with the form n:=ABC+DEF*GHI-1; by n:=f(ABC,DEF,GHI);
  (define (replace-assignments! s)
    (sg:traverse s
      ( (|StatementAssignment| _ rhs)
        (if (equal? rhs exp1)
          (sg:replace! rhs exp2))
        'skip)))

We end with two caveats about the functions introduced this section:

    • The first argument of sg:replace! must be a subterm of a buffer, i.e., it can not be a subterm of an arbitrary term.
    • Function(sg:string->term string phylum) is defined for most, but not all phyla. The grammar documents which phyla can be parsed.

Making Comments and Code Consistent

In well-written programs, comments and code are consistent. An Ada-ASSURED script can be used to check for such consistency. We develop a script that meets the following specification:

In each full-line comment of the form

        -- procedure body: <other text>

replace <other text> with the name of the next procedure specified or declared in the code.

The example is obviously scaled down for tutorial purposes, but illustrates many techniques required in more realistic settings.

The relevant portion of the Ada grammar dealing with full-line comments is:

      left_comment_line: LeftCommentLine( comment_text )
      comment_text: CommentText( STR STR )

Full-line comments are instances of operator LeftCommentLine, which contains a comment_text part. The comment_text part will be an instance of operator CommentText, which contains two STR parts. The first STR is the character immediately after the "--", and the second STR is the rest of the comment.

Function fix-comments traverses the contents of the buffer in preorder and calls do-replacement!at each full-line comment satisfying the relevant-header-comment? predicate.[24] The value of symbol $$ is the subterm currently matched by sg:traverse.

  ;;; Replace the contents of relevant header comments.
  (define (fix-comments)
     (sg:traverse (sg:buffer-term)
       ( (|LeftCommentLine| comment-text)
          (if (relevant-header-comment? comment-text)
          (do-replacement! $$) )
        'skip)))

Predicate relevant-header-comment? considers a comment relevant if it contains the substring "procedure body:". In practice, a more careful test would be written, e.g., taking the position of the substring into account. Function sg:term->string is used to turn c, the subterm containing the comment text, into a string.

  ;;; True if c contains substring "procedure body:", else false.
  (define (relevant-header-comment? c)
    (string-find? "procedure body:" (sg:term->string c)) )

Function do-replacement! computes the new comment by finding the name of the next procedure (proc-name), forming the appropriate comment text (new-text), and translating it into its term representation (new-term). The old comment (cur-term) is then replaced by the new comment (new-term) using sg:replace!. [25]

  ;;; Replace cur-term with "--procedure body: n",
  ;;; where n is the name of the next procedure in the code.
  (define (do-replacement! cur-term)
     (let* ((proc-name
          (next-procedure cur_term) )
         (new-text
          (string-append "procedure body: " proc-name) )
         (new-term
          (mk-comment-term new-text) ))
      (sg:replace! cur-term new-term) ))

Function next-procedure finds the name of the next procedure. The relevant portion of the Ada grammar is:[26]

        subprogram_specification: SubprogramSpecificationProcedure( expanded_name optional_formal_part )

Thus, next-procedure searches forward until it finds the next occurrence of operator SubprogramSpecificationProcedure. When the procedure specification is found, its name is converted to a string using sg:term->string, the name is assigned to local variable proc-name, and the traversal is aborted. If we didn't abort the traversal, proc-name would be set at every procedure in the traversed term, and next-procedure would return the name of the last procedure encountered, instead of the first. If no procedure is found, the empty string is returned.

  ;;; Return the name of the next procedure, if any, else "".
  (define (next-procedure comment-term)
    (let ((proc-name ""))
      (sg:ordered-traverse
        comment-term  ; Traverse comment-term
        'preorder    ; in preorder
        'left-to-right ; from left to right
        #t         ; continuing to the end of the context.
        #f         ; Don't repeat the pattern match at $$.
        ( (|SubprogramSpecificationProcedure| name params)
          (set! proc-name (sg:term->string name))
          'abort))
      proc-name))

We use sg:ordered-traverse to specify that the traversal should continue beyond the comment-term itself. Recall that (sg:traverse term (pattern body))only traverses the given term. But the procedure we seek will appear after, not within the comment-term. The fourth argument #t specifies that after completing the traversal of the term, the traversal continues all the way to the end of the surrounding context (unless terminated by 'abort).

Function mk-comment-term turns a string into a term of phylum left_comment_line. Because function sg:string->term does not support parsing strings as phylum left_comment_line, we build the term explicitly:

  (define left-comment-line
    (sg:string->operator "LeftCommentLine") )
  (define comment-text
    (sg:string->operator "CommentText") )
  ;;; Return the left-comment-line term for -- str
  (define (mk-comment-term str)
    (sg:build left-comment-line (sg:build comment-text " " str)))

Evaluating Scripts in Batch Mode

Scripts are often written to be run in batch mode using Ada-ASSURED's Batch Processing Module. Suppose the entire script has been saved in file fix_comments.stk. Then it can be executed on files f1.a and f2.a by running the following command from the shell:

  aa-batch -start "fix-comments" -save fix_comments.stk f1.a f2.a

More specifically, this command:

    • invokes command eval-file on file fix_comments.stk,
    • for each Ada file f1.a and f2.a:
    - invokes command open on the file,
    - invokes script function fix-comments,
    - invokes command save on the buffer,
    - invokes command close on the buffer.
    • invokes command exit.

Note that the same script function fix-comments can be used in either interactive or in batch modes.

Additional demonstration scripts are included with Ada-ASSURED and are described in the file $AAHOME/scripts/README.


Notes

1. Names in Scheme can include special characters like colon and hyphen. Thus, sg:write-message is one name.

Scheme has no formal notion of module or package. A collection of related names informally constituting a module are typically given related names. By convention, prefix m: is used for all global names of module m. The prefix sg: stands for Synthesizer Generator, technology that underlies Ada-ASSURED.

Names are case insensitive unless bracketed by vertical bars ( | ). Letters in names without brackets are mapped to lower case. Thus, SG:write-message is the same as sg:write-message, but |SG:write-message| is different from sg:write-message.

2. Expression (sg:write-message "Hello World") is a Scheme function call. In Scheme, the left parenthesis of a function call appears before, rather than after, the function name.

3. Expression (sg:save) is a Scheme function call. As with the previous example, the left parenthesis of the function call appears before the function name. Because function sg:save takes no arguments, the right parenthesis immediately follows the function name.

4. Expression (sg:save-as "Text" "foo" "BASEVIEW") is a Scheme function call. Function sg:save-as requires three string arguments. Note that arguments in a function call are not separated by commas.

5. There are no infix operators in Scheme. Even arithmetic operations are written in functional notation. Thus, what in Ada would be written as 2 + 3 * 4 is written in Scheme as (+ 2 (* 3 4)). Because there are no infix operators, parentheses are never used in Scheme to express precedence.

6. Type errors in Scheme expressions are detected dynamically during execution. Primitive functions, like -, check the types of their arguments on entry to the function.

7. Scheme construct (define name expression) declares variable name and sets its value to the value of expression. If name has already been declared, its value is overwritten. Function open-output-file takes a string argument, opens an output file with that name, and returns a port (i.e., a file descriptor) for that file. Function close-output-port takes a port argument and closes that port. Thus, the given script sets variable fd to a descriptor for the opened file /tmp/world, writes "Hello World" to that file, and closes the file.

8. STk function system takes a string argument and directs the given string to the system shell /bin/sh.

9. The general form of a Scheme function definition is (define(name parameters) expressions), where name is the name of the function, parameters is a list of zero or more parameter names, and expressions is a list of one or more expressions. When a function has several parameters, they are not separated by commas. Similarly, when the body of the function consists of several expressions, they are not separated by any punctuation marks. Running the given script defines function analyze, but does not call it.

10. In Scheme, arguments are passed to functions by value. In the call (sg:add-command "analyze" analyze), the value of the first argument is a string containing seven letters. The value of the second argument is the value of variable analyze, i.e., the function itself. In Scheme, functions are first class values. This example illustrates that functions can be passed to other functions as arguments. The argument is not the result of calling function analyze, but the definition of function analyze.

11. The character constant for the character c is written as #\c in Scheme.

12. One might well ask why there is no syntactic category for Scheme programs so scripts could be edited with a Scheme structure editor. Such a capability is planned for a future Ada-ASSURED release.

13. In Scheme, if name already exists, evaluating (define (name parameters) expressions) or (define name expression) redefines name. Similarly, sg:add-command will redefine an existing command.

14. Expression (let ((v1 e1) ... (vn en)) body) declares local variables v1, ..., vn and initializes them to the values of expressions e1, ..., en, respectively. Body is a list of one or more expressions. The value of the let-expression is the value of the last expression in the body. A let-expression is similar to an Ada block: it introduces a local scope that contains initialized variable declarations. Unlike Ada, the variables are typeless and the block has a value.

Evaluating expression (set! v e) stores the value of expression e in variable v. The exclamation point in set! is part of the name of the assignment operation. It is a convention in Scheme to give operations with side effects names ending with the emphatic ! character.

15. A single semicolon signifies that the rest of the line is a comment. It is a common convention to repeat the semicolon (as the first few characters of the comment) for emphasis.

16. Function number-string converts a Scheme number (like the count returned by count-nodes) to a string (as required by sg:write-message).

17. Usually, an identifier refers to a variable with the given name. For example, the identifier count refers to a variable named count. In Scheme, a identifier itself can also be a legitimate value. Such values are known as symbols. To denote the symbol skip rather than a variable named skip, the identifier skip must be quoted; otherwise, the interpreter will look for a variable named skip to evaluate. Writing 'skip is equivalent to writing (quote skip).

18. The list containing 0 elements, known as the empty list, is denoted '(). If h is any Scheme value and t is a list, then the value of expression (cons h t) is the list with first element h and remaining elements t. For example, expression (cons 1 (cons 2 (cons 3 (cons 4 '() )))) constructs the list (1 2 3 4).

19. Function (make-hash-table function) creates an empty hash table that uses the binary predicate function to test equality between index keys. Function string=? is Scheme's built-in equality predicate between strings.

Function (sg:load-html-file path file) directs Ada-ASSURED's HTML browser to open path/file. Function (getcwd) returns the path of the current working directory as a string.

20. Function (hash-table-get table key default) returns the hash table entry associated with key in table, if present, and otherwise returns the value default.

Predicate (integer? e) returns #t (i.e., true) if the value of expression e is integer, and otherwise returns #f (i.e., false).

Function (hash-table-put! table key e) associates the value of expression e with key in table, deleting any other association for key.

21. Function (format port string e1 ... en) outputs the values of expressions e1 ... en to port according to the format string. The values are displayed left-to-right at the occurrences of ~a in the format string. Each ~% in string signifies a newline.

Function (hash-table-map table function) applies function to each item in table. Function must be a function of two arguments: the key of the item and its associated value. In the example, the function that is passed to hash-table-map is created using form (lambda arguments body). Function
     (lambda (s c)(bean:html-counts port s c))
is an unnamed function of two arguments, s and c, that calls function bean:html-counts with the three arguments port, s and c. Variable port, which is declared in a surrounding lexical scope, is accessed by the anonymous function as a nonlocal variable. This is analogous to what is possible with nested function declarations in Ada, where the inner function can access the local variables of the enclosing function.

22. Function (exec string) invokes the program string and returns its return value. Function (getenv string) returns the value of environment variable string.

23. Expression (if expression0 expression1 expression2) is Scheme's conditional expression. If the value of expression0 is #t, i.e., true, the value of expression1 is returned, otherwise the value of expression2 is returned. If expression2 is omitted and the value of expression0 is not #t, then the value of the conditional expression is unspecified.

Expression (equal? expression0 expression1) tests equality between two Scheme values. Two structured values are equal if they have the same shape. Function equal? does not require that its two arguments be identical values, i.e., one and the same value, just that they have the same shapes.

24. The question mark in relevant-header-comment? is part of the name of the function. It is a convention in Scheme to give functions that return #t (i.e., true) or #f (i.e., false) names ending with the quizzical ? character.

25. Expression (let* ((v1 e1) ... (vn en)) body) is similar to a regular let without the *. The difference is that in a let*, occurrences of variables v1, ..., vn in expressions e1, ..., en refer to the new variables declared in the let*, whereas in (let ((v1 e1) ... (vn en)) body), occurrences of variables v1, ..., vn in expressions e1, ..., en refer to variables declared in a surrounding scope.

26. This is the rule for Ada 95. The corresponding rule for Ada 83 is has name in place of expanded_name.