The assembler calls make_execution_function to generate the execution function for an instruction. Like the analyze function in the evaluator of section 4.1.7, this dispatches on the type of instruction to generate the appropriate execution function.
function make_execution_function(inst, labels, machine, pc, flag, stack, ops) { const x = head(inst); return x === "assign" ? make_assign(inst, machine, labels, ops, pc) : x === "test" ? make_test(inst, machine, labels, ops, flag, pc) : x === "branch" ? make_branch(inst, machine, labels, flag, pc) : x === "go_to" ? make_goto(inst, machine, labels, pc) : x === "save" ? make_save(inst, machine, stack, pc) : x === "restore" ? make_restore(inst, machine, stack, pc) : x === "perform" ? make_perform(inst, machine, labels, ops, pc) : error(inst, "Unknown instruction type: ASSEMBLE"); }
For each type of instruction in the register-machine language, there is a generator that builds an appropriate execution function. The details of these functions determine both the syntax and meaning of the individual instructions in the register-machine language. We use data abstraction to isolate the detailed syntax of register-machine expressions from the general execution mechanism, as we did for evaluators in section 4.1.2, by using syntax functions to extract and classify the parts of an instruction.
The make_assign function handles assign instructions:
function make_assign(inst, machine, labels, operations, pc) { const target = get_register(machine, assign_reg_name(inst)); const value_exp = assign_value_exp(inst); const value_fun = is_operation_exp(value_exp) ? make_operation_exp(value_exp, machine, labels, operations) : make_primitive_exp(head(value_exp), machine, labels); function perform_make_assign() { set_contents(target, value_fun()); advance_pc(pc); } return perform_make_assign; }
function assign_reg_name(assign_instruction) { return head(tail(assign_instruction)); } function assign_value_exp(assign_instruction) { return tail(tail(assign_instruction)); }
The register name is looked up with get_register to produce the target register object. The value expression is passed to make_operation_exp if the value is the result of an operation, and to make_primitive_exp otherwise. These functions (shown below) parse the value expression and produce an execution function for the value. This is a function of no arguments, called value_fun, which will be evaluated during the simulation to produce the actual value to be assigned to the register. Notice that the work of looking up the register name and parsing the value expression is performed just once, at assembly time, not every time the instruction is simulated. This saving of work is the reason we use execution functions, and corresponds directly to the saving in work we obtained by separating program analysis from execution in the evaluator of section 4.1.7.
The result returned by make_assign is the execution function for the assign instruction. When this function is called (by the machine model's execute function), it sets the contents of the target register to the result obtained by executing value_fun. Then it advances the pc to the next instruction by running the function
function advance_pc(pc) { set_contents(pc, tail(get_contents(pc))); }
Make_test handles test instructions in a similar way. It extracts the expression that specifies the condition to be tested and generates an execution function for it. At simulation time, the function for the condition is called, the result is assigned to the flag register, and the pc is advanced:
function make_test(inst, machine, labels, operations, flag, pc) { const condition = test_condition(inst); if (is_operation_exp(condition)) { const condition_fun = make_operation_exp(condition, machine, labels, operations); function perform_make_test() { set_contents(flag, condition_fun()); advance_pc(pc); } return perform_make_test; } else { error(inst, "Bad TEST instruction: ASSEMBLE"); } } function test_condition(test_instruction) { return tail(test_instruction); }
The execution function for a branch instruction checks the contents of the flag register and either sets the contents of the pc to the branch destination (if the branch is taken) or else just advances the pc (if the branch is not taken). Notice that the indicated destination in a branch instruction must be a label, and the make-branch function enforces this. Notice also that the label is looked up at assembly time, not each time the branch instruction is simulated.
function make_branch(inst, machine, labels, flag, pc) { const dest = branch_dest(inst); if (is_label_exp(dest)) { const insts = lookup_label(labels, label_exp_label(dest)); function perform_make_branch() { if (get_contents(flag)) { set_contents(pc, insts); } else { advance_pc(pc); } } return perform_make_branch; } else { error(inst, "Bad BRANCH instruction: ASSEMBLE"); } } function branch_dest(branch_instruction) { return head(tail(branch_instruction)); }
A go_to instruction is similar to a branch, except that the destination may be specified either as a label or as a register, and there is no condition to check—the pc is always set to the new destination.
function make_goto(inst, machine, labels, pc) { const dest = goto_dest(inst); if (is_label_exp(dest)) { const insts = lookup_label(labels, label_exp_label(dest)); return () => set_contents(pc, insts); } else if (is_register_exp(dest)) { const reg = get_register(machine, register_exp_reg(dest)); return () => set_contents(pc, get_contents(reg)); } else { error(inst, "Bad GOTO instruction: ASSEMBLE"); } } function goto_dest(goto_instruction) { return head(tail(goto_instruction)); }
The stack instructions save and restore simply use the stack with the designated register and advance the pc:
function make_save(inst, machine, stack, pc) { const reg = get_register(machine, stack_inst_reg_name(inst)); function perform_make_save() { push(stack, get_contents(reg)); advance_pc(pc); } return perform_make_save; } function make_restore(inst, machine, stack, pc) { const reg = get_register(machine, stack_inst_reg_name(inst)); function perform_make_restore() { set_contents(reg, pop(stack)); advance_pc(pc); } return perform_make_restore; } function stack_inst_reg_name(stack_instruction) { return head(tail(stack_instruction)); }
The final instruction type, handled by make_perform, generates an execution function for the action to be performed. At simulation time, the action function is executed and the pc advanced.
function make_perform(inst, machine, labels, operations, pc) { const action = perform_action(inst); if (is_operation_exp(action)) { const action_fun = make_operation_exp(action, machine, labels, operations); return () => { action_fun(); advance_pc(pc); } } else { error(inst, "Bad PERFORM instruction: ASSEMBLE"); } } function perform_action(inst) { return tail(inst); }
The value of a reg, label, or constant expression may be needed for assignment to a register (make_assign) or for input to an operation (make_operation_exp, below). The following function generates execution functions to produce values for these expressions during the simulation:
function make_primitive_exp(exp, machine, labels) { if (is_constant_exp(exp)) { const c = constant_exp_value(exp); return () => c; } else if (is_label_exp(exp)) { const insts = lookup_label(labels, label_exp_label(exp)); return () => insts; } else if (is_register_exp(exp)) { const r = get_register(machine, register_exp_reg(exp)); return () => get_contents(r); } else { error(exp, "Unknown expression type: ASSEMBLE"); } }
The syntax of reg, label, and constant expressions is determined by
function is_register_exp(exp) { return is_tagged_list(exp, "reg"); } function register_exp_reg(exp) { return head(tail(exp)); } function is_constant_exp(exp) { return is_tagged_list(exp, "constant"); } function constant_exp_value(exp) { return head(tail(exp)); } function is_label_exp(exp) { return is_tagged_list(exp, "label"); } function label_exp_label(exp) { return head(tail(exp)); }
Assign, perform, and test instructions
may include the application of a machine operation (specified by
an op expression) to some operands (specified by reg
and constant expressions).
The following
function
produces an execution
function
for an operation expression
—a list containing the operation and
operand expressions from the instruction:
function make_operation_exp(exp, machine, labels, operations) { const op = lookup_prim(operation_exp_op(exp), operations); const aprocs = map(e => make_primitive_exp(e, machine, labels), operation_exp_operands(exp)); function perform_make_operation_exp() { return op(map(p => p(), aprocs)); } return perform_make_operation_exp; }
The syntax of operation expressions is determined by
function is_operation_exp(exp) { return is_pair(exp) && is_tagged_list(head(exp), "op"); } function operation_exp_op(operation_exp) { return head(tail(head(operation_exp))); } function operation_exp_operands(operation_exp) { return tail(operation_exp); }
Observe that the treatment of operation expressions is very much like the treatment of function applications by the analyze_application function in the evaluator of section 4.1.7 in that we generate an execution function for each operand. At simulation time, we call the operand functions and apply the Scheme function that simulates the operation to the resulting values. The simulation function is found by looking up the operation name in the operation table for the machine:
function lookup_prim(symbol, operations) { const val = assoc(symbol, operations); return val === undefined ? error(symbol, "Unknown operation: ASSEMBLE") : head(tail(val)); }
save(y); save(x); restore(y);