How executable functions are expressed in textual inter programs.


§1. Code packages. To recap: a file of textual inter has a brief global section at the top, and is then a hierarchiy of package definitions. Each package begins with a symbols table, but then has contents which depend on its type. This section covers the possible contents for a code package, that is, one whose type is _code.

Note that package is a data statement, not a code statement, and it follows that there is no way for a code package to contain sub-packages. Conceptually, a code package is a single executable function.

§2. Local variables. The statement local NAME KIND gives the code package a local variable; the NAME must be a private misc symbol in the package's symbols table. Local variable definitions should be made after the symbols table and before the code statement. When the function is called at run-time, its earliest-defined locals will hold any arguments from the function call. So for example, if:

    package Double _code
        symbol private misc x
        local x K_number

then a call Double(6) would begin executing with x equal to 6.

§3. Labels. Like labels in any assembly language, these are named reference points in the code; they are written NAME, where NAME must be a private label symbol in the package's symbols table, and must begin with a full stop ..

All code packages must contain a "code" node at the top level, which is the final statement in the package, and contains the actual body code.

    package HelloWorld _code
        code
            inv !print
                val K_text "Hello World!\n"

§4. Primitive invocations. Other than labels and locals, code is a series of "invocations". An invocation is a use of either another function, or of a primitive.

Recall that the global section at the top of the inter file will likely have declared a number of primitives, with the notation:

    primitive PRIMITIVE IN -> OUT

Primitives are, in effect, built-in functions. IN can either be void or can be a list of one or more terms which are all either ref, val, lab or code. OUT can be either void or else a single term which is either ref or val. For example,

    primitive !plus val val -> val

declares that the signature of the primitive !plus is val val -> val, meaning that it takes two values and produces another as result, while

    primitive !ifelse val code code -> void

says that !ifelse consumes a value and two blocks of code, and produces nothing. Of course, !plus adds the values, whereas !ifelse evaluates the value and then executes one of the two code blocks depending on the result. But the statement primitive specifies only names and signatures, not meanings.

The third term type, lab, means "the name of a label". This must be an explicit name: labels are not values, which is why the signature for !jump is primitive !jump lab -> void and not primitive !jump val -> void.

The final term type, ref, means "a reference to a value", and is in effect an lvalue rather than an rvalue: for example,

    primitive !pull ref -> void

is the prototype of a primitive which pulls a value from the stack and stores it in whatever is referred to by the ref (typically, a variable).

Convention. Inform defines a standard set of around 90 primitives. Although their names and prototypes are not part of the inter specification as such, you will only be able to use Inter's "compile to I6" feature if those are the primitives you use, so in effect this is the standard set. Details of these primitives and what they do will appear below.

§5. A primitive is invoked by inv PRIMITIVE, with any necessary inputs, matching the IN part of its signature, occurring on subsequent lines indented one tab stop in. For example:

    inv !plus
        val K_number 2
        val K_number 2

would compute 2+2. This brings up the issue of "context". Code statements are always parsed in a given context, the context being what they are expected to produce or do. In this example:

    inv !print
        val K_text "Hello World!\n"

the invocation of !print occurs at the top level of the function, in what is called "void" context; but the val statement occurs in value context, because it appears where the !print invocation expects to find a text value. It is an error to use a statement in the wrong context. For example, this:

        code
            inv !plus
                val K_number 2
                val K_number 2

is an error, because it makes no sense to evaluate !plus in a void context: the sum would just be thrown away unused. The context in which an inv statement is allowed depends on the OUT part of the signature of its primitive. Comparing the declarations of !print and !plus, we see:

    primitive !print val -> void
    primitive !plus val val -> val

so that !print can only be invoked in a void context, and !plus in a value context.

§6. Function invocations. The same statement, inv, is also used to call functions which are not primitive: that is, functions which are defined by code packages.

To do so, though, we need a value identifying the function. This is done as follows. Suppose:

    kind K_number int32
    kind K_number_to_number K_number -> K_number
    package Double_B _code
        symbol private misc x
        local x K_number
        code
            inv !return
                inv !plus
                    val K_number x
                    val K_number x
    constant Double K_number_to_number = Double_B

The value Double now evaluates to this function, and that's what we can invoke. Thus:

    inv Double
        val K_number 17

compiles to a function call returning the number 34.

It would not make sense, and is an error, to write inv Double_B, because Double_B is a package name, not a value; and because there is no way to know its signature. By contrast, Double is indeed a value, and by looking at its kind K_number_to_number, we can see that the signature for the invocation must be val -> val.

§7. Val and cast. As has already been seen in the above examples, val KIND VALUE can be used in value context to supply an argument for a function or primitive.

In general, inter code has very weak type checking. val KIND VALUE forces the VALUE to conform to the KIND, but no check is made on whether this kind is appropriate in the current context. For example, the primitive !print requires its one value to be textual, so that the following:

    inv !print
        val K_number 7

has undefined behaviour at run-time. Though a terrible idea, this is valid inter code. This code, on the other hand, will throw an error:

    inv !print
        val K_number "Seven"

The inter language does nevertheless provide for compilers which want to produce much stricter type-checked code, or which need their code-generators to compile shim code converting values between kinds. The statement:

    cast KIND1 <- KIND2

is valid only in value context, and marks that a value of KIND2 needs to be interpreted in some way as a value of KIND1. For example, one might imagine something like this:

    inv !times
        cast K_number <- K_truth_state
            val K_truth_state flag1
        cast K_number <- K_truth_state
            val K_truth_state flag2

§8. Ref, lab and code. Just as val supplies a value as needed by a val term in an invocation signature, so ref, lab and code meet the other possible requirements. For example, suppose the following signatures:

    primitive !jump lab -> void
    primitive !pull ref -> val
    primitive !if val code -> void

These might be invoked as follows:

    inv !jump
        lab .end

Here .end is the name of a label in the current function. References to labels in other functions are impossible, because label names are all private to the current symbols table. No kind is mentioned in a lab statement because labels are not values, and therefore do not have kinds.

    inv !pull
        ref K_number x

Here x is the name of a variable, but it could be the name of any form of storage. On the other hand, ref K_number 10 would be an error, because it isn't possible to write to the number 10: that is an rvalue but not an lvalue.

    inv !if
        val K_truth_state flag
        code
            inv !print
                val K_text "Yes!"

compiles to something like if (flag) { print "Yes!"; }. The code statement is similar to a braced code block in a C-like language. Any amount of code can appear inside it, indented by one further tab stop; this code is all read in void context. There is no such thing as a code block which returns a value, and code can only be used in code context (i.e. matching the signature term code), not in value context. If what you need is code to return a value, that should be another function.

§9. Evaluation and reference. Using the mechanisms above, there is no good way to throw away an unwanted value: it is an error, for example, to evaluate something in void context. That's unfortunate if we want to evaluate something not for its result but for a side-effect. To get around that, we have:

    evaluation
        ...

evaluation causes any number of indented values to be evaluated, throwing each result away in turn. In effect, it's a shim which changes the context from void context to value context; it tends to generate no code in the final program.

reference is similarly a shim, but from reference context to value context. This is not in general a safe thing to do: consider the consequences of the following, for example -

    inv !store
        reference
            val K_number 7
        val K_number 3

In general reference must only be used where it can be proved that its content will compile to an lvalue in the Inform 6 generated. Inform uses it as little as possible.