The machine model generated by make_machine is represented as a function with local state using the message-passing techniques developed in chapter 3. To build this model, make_machine begins by calling the function make_new_machine to construct the parts of the machine model that are common to all register machines. This basic machine model constructed by make_new_machine is essentially a container for some registers and a stack, together with an execution mechanism that processes the controller instructions one by one.
The function make_machine then extends this basic model (by sending it messages) to include the registers, operations, and controller of the particular machine being defined. First it allocates a register in the new machine for each of the supplied register names and installs the designated operations in the machine. Then it uses an assembler (described below in section 5.2.2) to transform the controller list into instructions for the new machine and installs these as the machine's instruction sequence. The function make_machine returns as its value the modified machine model.
function make_machine(register_names, ops, controller) {
const machine = make_new_machine();
for_each(register_name =>
machine("allocate_register")(register_name),
register_names);
machine("install_operations")(ops);
machine("install_instruction_sequence")
(assemble(controller, machine));
return machine;
}
We will represent a register as a function with local state, as in chapter 3. The function make_register creates a register that holds a value that can be accessed or changed:
function make_register(name) {
let contents = "*unassigned*";
function dispatch(message) {
return message === "get"
? contents
: message === "set"
? value => { contents = value; }
: error(message, "unknown request -- make_register");
}
return dispatch;
}
function get_contents(register) {
return register("get");
}
function set_contents(register, value) {
return register("set")(value);
}
We can also represent a stack as a function with local state. The function make_stack creates a stack whose local state consists of a list of the items on the stack. A stack accepts requests to push an item onto the stack, to pop the top item off the stack and return it, and to initialize the stack to empty.
function make_stack() {
let stack = null;
function push(x) {
stack = pair(x, stack);
return "done";
}
function pop() {
if (is_null(stack)) {
error("empty stack -- pop");
} else {
const top = head(stack);
stack = tail(stack);
return top;
}
}
function initialize() {
stack = null;
return "done";
}
function dispatch(message) {
return message === "push"
? push
: message === "pop"
? pop()
: message === "initialize"
? initialize()
: error(message, "unknown request -- stack");
}
return dispatch;
}
function pop(stack) {
return stack("pop");
}
function push(stack, value) {
return stack("push")(value);
}
The
make_new_machine
function,
shown in figure 5.16, constructs an
object whose local state consists of a stack, an initially empty instruction
sequence, a list of operations that initially contains an operation to
initialize the stack, and a
register table that initially contains two
registers, named
flag and
pc
(for program counter
). The internal
function
allocate_register
adds new entries to the register table, and the internal
function
lookup_register
looks up registers in the table.
function make_new_machine() {
const pc = make_register("pc");
const flag = make_register("flag");
const stack = make_stack();
let the_instruction_sequence = null;
let the_ops = list(list("initialize_stack", () => stack("initialize")));
let register_table = list(list("pc", pc), list("flag", flag));
function allocate_register(name) {
if (is_undefined(assoc(name, register_table))) {
register_table = pair(list(name, make_register(name)),
register_table);
} else {
error(name, "multiply defined register");
}
return "register allocated";
}
function lookup_register(name) {
const val = assoc(name, register_table);
return is_undefined(val)
? error(name, "unknown register")
: head(tail(val));
}
function execute() {
const insts = get_contents(pc);
if (is_null(insts)) {
return "done";
} else {
inst_execution_fun(head(insts))();
return execute();
}
}
function dispatch(message) {
function start() {
set_contents(pc, the_instruction_sequence);
return execute();
}
return message === "start"
? start()
: message === "install_instruction_sequence"
? seq => { the_instruction_sequence = seq; }
: message === "allocate_register"
? allocate_register
: message === "get_register"
? lookup_register
: message === "install_operations"
? ops => { the_ops = append(the_ops, ops); }
: message === "stack"
? stack
: message === "operations"
? the_ops
: error(message, "unknown request -- machine");
}
return dispatch;
}
The flag register is used to control branching in the simulated machine. Our test instructions set the contents of flag to the result of the test (true or false). Our branch instructions decide whether or not to branch by examining the contents of flag.
The pc register determines the sequencing of instructions as the machine runs. This sequencing is implemented by the internal function execute. In the simulation model, each machine instruction is a data structure that includes a function of no arguments, called the instruction execution function, such that calling this function simulates executing the instruction. As the simulation runs, pc points to the place in the instruction sequence beginning with the next instruction to be executed. The function execute gets that instruction, executes it by calling the instruction execution function, and repeats this cycle until there are no more instructions to execute (i.e., until pc points to the end of the instruction sequence).
As part of its operation, each instruction execution function modifies pc to indicate the next instruction to be executed. The instructions branch and go_to change pc to point to the new destination. All other instructions simply advance pc, making it point to the next instruction in the sequence. Observe that each call to execute calls execute again, but this does not produce an infinite loop because running the instruction execution function changes the contents of pc.
The function make_new_machine returns a dispatch function that implements message-passing access to the internal state. Notice that starting the machine is accomplished by setting pc to the beginning of the instruction sequence and calling execute.
For convenience, we provide an alternate interface to a machine's start operation, as well as functions to set and examine register contents, as specified at the beginning of section 5.2:
function start(machine) {
return machine("start");
}
function get_register_contents(machine, register_name) {
return get_contents(get_register(machine, register_name));
}
function set_register_contents(machine, register_name, value) {
set_contents(get_register(machine, register_name), value);
return "done";
}
function get_register(machine, reg_name) {
return machine("get_register")(reg_name);
}