CS 331 Spring 2025  >  Programming-Language Execution Models


CS 331 Spring 2025
Programming-Language Execution Models

Here we look at what drives the execution of a program. What task is it accomplishing? We take a high-level point of view; so we will not discuss implementation details.

Introduction

Recall that when a program is executed, the computations it specifies actually occur.

In all cases, something drives the execution.

Both of these can vary significantly from one programming language to another. Together, they are part of a programming language’s execution model, which specifies all the details of how a program is executed.

We look at these for various PLs, in preparation for the study of a PL in which both the task driving the execution and the strategy may differ significantly from what you have seen before.

Commands

For example, consider the C programming language.

Q. When executing a C program, what task drives the execution?
A. A call to function main. The entirety of the execution of a C program is the computer endeavoring to complete this function call.

Q. In C, what strategy is employed in executing?
A. The computer is given a sequence of commands. It carries out each, in order, performing the action that each specifies. If a command specifies that another function is to be called, then this becomes a subtask to be completed as part of the primary task.

Now consider Lua.

Q. What task drives the execution of a Lua program?
A. The code at global scope in a source file is executed.

Q. What strategy is employed?
A. Much the same strategy as that for a C program. The computer is given a sequence of commands. It carries out each, in order, performing the action that each specifies. If a command specifies that another function is to be called, then this becomes a subtask to be completed as part of the primary task.

Answers are similar for C++, Java, and Forth—and many other programming languages aimed at imperative programming: programming in which we tell the computer what to do.

Evaluation

What about Haskell?

Haskell code does not consist of commands to be carried out, but rather expressions to be evaluated. Haskell is much more oriented toward declarative programming than those mentioned earlier: we tell the computer what the values of things are, rather than what to do. Haskell code does not consist of commands to be carried out, but rather expressions to be evaluated.

Q. What task drives the execution of a Haskell program.
A. An expression is to evaluated. For a complete Haskell program, this expression is Main.main. But Haskell allows for the evaluation of other expressions to drive execution. Regardless, the entirety of the execution of a Haskell program is the computer endeavoring to evaluate the expression.

Q. What strategy is employed?
A. The primary function/operator in the expression is called. If it requires the evaluation of subexpressions, then those evaluations are carried out as subtasks. Haskell is a pure programming language—it has no side effects—so Haskell execution consists entirely of expression evaluation.

But there is another element to Haskell execution.

Haskell I/O actions are values that are descriptions of side effects. These are returned to the runtime environment, which performs the side effects. Some I/O actions may include code to be executed with the wrapped value. Such code is also executed. This is how things like the following are handled.

[Haskell]

do
    line <- getLine
    putStrLn $ reverse Line

The call to reverse can only happen after a side effect has been performed. Such code must be called by the runtime.

Unification

We have discussed strategies involving carrying out commands, and strategies involving evaluation of expressions. But there are other possibilities, one of which we introduce now: unification.

To unify two constructions means to make them the same by binding variables (setting their values) as necessary.

Here are some examples. Upper-case letters are variables.

Q. Can we unify X and 8?
A. Yes. Set X to 8.

Q. Can we unify 5 and 8?
A. No, these cannot be unified.

Q. Can we unify 5 and 5?
A. Yes, these can be unified. They are already the same.

Let’s write lists as in Haskell: comma-separated values enclosed in brackets.

Q. Can we unify X and the two-item list [1, 7]?
A. Yes. Set X to [1, 7].

We allow lists of free variables. As part of unification, we might bind these variables.

Q. Can we unify [A, B] and [1, 7]?
A. Yes. Set A to 1, and B to 7.

Q. Can we unify N and 3+5?
A. Yes, but what exaclty we do depends on what we mean by making them “the same”. We might be inclined to set N to 8. However, while 3+5 and 8 have the same numeric value, one is an expression involving addition, while the other is simple a number. So let us unify N and 3+5 by setting N to 3+5, with evaluation as a later option.

In full generality, unification includes assignment [X = …] as a special case. But unification can do things that assignment cannot do.

Q. Can we unify [A, 6] and [4, B]?
A. Yes. Set A to 4, and B to 6.

Unification in Prolog

Unification can be the basis of another execution strategy.

As we will see, in the Prolog programming language, execution is driven by a query: essentially a question, which can be either yes/no or “What/who … ?”. And the execution strategy is based on unification.

Prolog is another programming language aimed at declarative programming. But instead of telling it what the values of things are, as in Haskell (and also Scheme), we tell it what is true. A Prolog program is a knowledge base. When given a query, Prolog attempts to unify the query with something in its knowledge base.

For example, we might write “A is the child of B” in Prolog as child(A, B).

Now consider a Prolog program (knowledge base) that consists of the following seven facts.

[Prolog]

child(bill, molly).
child(charlie, molly).
child(percy, molly).
child(fred, molly).
child(george, molly).
child(ron, molly).
child(ginny, molly).

We require variables to begin with upper-case letters. So child, bill, molly, and so on are not variables. Think of them as being something like strings.

We can ask, "Is Fred the child of Molly?" by doing a query child(fred, molly). We get a Yes answer, because there is something in the knowledge base that the query can be unified with. Obviously, child(fred, molly) can be unified with child(fred, molly), since they are already the same.

We can ask, "Is Harry the child of Molly?" by doing a query child(harry, molly). We get a No answer, as there is nothing in the knowledge base that the query can be unified with.

Suppose we wish to ask, “Who are the children of Molly?” We can write a query child(X, molly); note that X is a free variable. There are seven facts in the knowledge base that this query can be unified with, by setting X to bill, charlie, percy, fred, george, ron, and ginny, respectively, and this is the answer Prolog gives us.

The punchline is that Prolog does all execution in this way. The task driving execution is to answer some query. The strategy for completing that task involves unifying the query with something known. The full details of how that works will take some explanation; we will get there eventually.