<<

Module 7: Functions in the virtual machine

Contents how to implement a dif- ferent from vScheme, and you’ll sketch a proto- Introduction 1 type of one component. You deserve a chance to practice beforehand, and I have planned such The module step by step 1 practice for next week. As raw material for that Before lab ...... 1 practice, you’ll select one language feature from Lab ...... 2 among those proposed—so the proposals need to After lab ...... 3 come in this week. Tail calls ...... 3 Stack overflow ...... 4 • How? Application to other languages ...... 4 What and how to submit ...... 5 – Before lab you’ll do some reading on procedure Overview of the code 5 calls and register windows. New code you will write or edit ...... 5 Code you will extend ...... 5 – Before lab you’ll also get your SVM and UFT ready: to prepare to use your lab time produc- Learning outcomes 5 tively, you’ll tell the SVM how to load instruc- Outcomes available for points ...... 5 tions call, tailcall, and return; you’ll set up How to claim the project points ...... 6 placeholders for call, return, and tailcall in- structions in your vmrun function; and you’ll en- able idump in vmrun (if you haven’t al- Introduction ready). This week you’ll head back to the virtual machine. You’ll And so you can try using the assembly code implement register windows and function calls, including distributed to support the lab, you’ll tell optimized tail calls. your assembly-language parser that blank lines • What am I doing? are OK. – Add support for functions to your VM state: reg- ister windows and a . – In lab you’ll extend your VM state with a call stack and a register-window pointer, and you’ll – Implement call, tailcall, and return instruc- implement the call instruction (for calling VM tions. functions only). There’s a decent chance that you also can do the return instruction. – Propose three language features for finale practice next week. – After lab, you’ll implement tail calls, and you’ll • Why am I doing it? add overflow checks to both call and tailcall instructions. – A high-level language needs functions (or meth- ods). – Also after lab, you’ll propose three language fea- – Register windows are a fabulous trick that is very tures, found in three different languages, that efficient and that enables us to avoid worrying might be interesting to add to our implementa- about and spilling in the UFT. tion. – A long-term goal of the course is that you be able to adapt your UFT and SVM to implement a – At the end of the week, you’ll deliver a virtual ma- language of your choice. That goal will be as- chine that works with first-order functions. Plus sessed in the course finale, where you’ll analyze your language-feature proposals.

1 The module step by step in .vo Parser Unparsing template Before lab call parseR3 rX := call rY (rY+1, ..., rZ) (1) Review hardware procedure calls (recommended). return parseR1 return rX You’ll want to remind yourself how calls and returns tailcall parseR2 tailcall rX (rX+1, ..., rY) work and how they interact through a call stack. A re- minder about passing parameters in registers won’t hurt either. (4) Add procedure-call cases to vmrun (essential). For each opcode you added in the previous step, add a case to If you’re not sure what to review, here are a couple of your vmrun function. For now, each case can simply suggestions: assert(0). • For a high-level view of a call stack, there’s a de- (5) Make sure your SVM has a debugging mode (recom- cent short video from CS 50. What the speaker mended). Check your vmrun function to be sure you can calls “a procedure on pause” is more usually called configure it to call idump on every instruction. Here’s “a suspended activation.” an example of how to use debugging mode and what the results look like, computing four factorial: • For a slightly lower-level view that mentions ma- chine registers, complete with a little animation, Trace of SVM computing and check- there’s a blog post by Connor Stack. ing four factorial (click arrow to reveal) • You could just dive straight into the comparison section of the procedure-call handout, which in- cludes an example of code compiled to AMD64 . (2) Read about how to implement calls (essential). Imple- menting calls is the entire module, and I don’t expect you to have grasped it all before lab. But in order to be productive during lab, you will need to have some idea what is going on. 1. To develop your overall understanding, read my handout on Understanding Procedure Calls. 2. To master the details of instructions’ behav- ior and coding, read the operational semantics. Before lab, take notes on two topics: • What goes into an activation (a frame on the call stack) and how you want to represent it • The actions of a call instruction For both sets of notes, the operational semantics will be useful. To turn this mode on in your own SVM, you’ll need to take these steps: (3) Add procedure-call instructions to the SVM (es- sential). In lab, you’ll implement instructions you can • In vmrun, initialize two variables ones when vmrun run, but you need to come into lab already being able starts: to load them. const char *dump_decode = svmdebug_value("decode"); You need three instructions: call, , and return. const char *dump_call = svmdebug_value("call"); Each needs an internal and an external opcode, a pars- (void) dump_call; // make it OK not to use `dump_call` ing function, and an unparsing template. The choices • In vmrun, immediately after decoding an instruction, I recommend are as follows: before using switch on the opcode, if dump_decode is set, call idump: Add the to opcodes.h and the instructions to instructions.c. if (dump_decode)

2 but you probably want capabilities that are compara- vm The VM state ble to other interpreters. For the call stack, here are pc The current (as an integer) some data points: instr The instruction word just decoded • The interpreters used in COMP 105 support re- window_position An integer saying where the current register window pointscursion to depth of around 5000 (the exact num- pRX, pRY, pRZ Pointers to the values stored in registers X, Y, and Z ber can depend on the size of a single activation record).

idump(stderr, vm, pc, instr, window_position, pRX, pRY, pRZ);• The standard for the Icon program- ming language defaults to a stack of 10,000 words, The parameters are as follows: which I’m guessing might be ballpark of 2,000 ac- The window_position can be any number that mean- tivations. ingfully tells you the position of the register windows. • In 2010, a standard Python interpreter had a de- All idump does with it is print it. fault stack limit of 1000 activations. • If you want to write out additional information at For the register file, I don’t have data, but 10 registers call, return, and tail-call instructions, do so only when per activation might be in the right ballpark. dump_call is not NULL. (9) Implement call. Implement the call instruction. In- To make these codes compile, your vmrun.c will need formally, this instruction should push a new activation to #include both "svmdebug.h" and "disasm.h". onto the call stack, shift the register window, and start (6) Update your assembly-language parser to support blank executing the new function from instruction 0. For a lines (lightly recommended). In file asmparse.sml, precise specification, consult the operational se- mantics. • In the instruction parser, change each single occur- rence of eol to accept one or more newlines: many1 eol. If you’re not sure how to approach this step, especially if you skipped ahead from step (7), ask yourself these • Change function parse to accept initial blank lines: questions:

val parse = • How do I transfer control to the function I wish Error.join o to call (the “callee”)? P.produce (Error.list <$> (many eol >> many instruction)) o • How do I adjust the register window to ensure List.concat that the callee sees its own value in register 0, its These changes will enable your parser to handle assem- first parameter in register 1, and soon? bly code with blank lines and comment lines. • What do I need to remember so that when the Recompile your UFT. call completes, I can continue executing my own code (in the caller)? Lab It’s the stuff you need to remember that goes into an activation record. (7) Define a procedure activation. Using abstract-machine state from the operational semantics, put fields in the The answers to the questions themselves are mostly definition of struct Activation in file vmstack.h. found in the operational semantics. It helps to relate the notations in the operational semantics to the vari- (If you’re not sure how to get started here, you could ables in the code: what is 푅, what is 퐼, and so on. consider jumping forward to step (9) and then coming back.) One issue not specified in the operational semantics is that an SVM has finite space for procedure activations (8) Enlarge the VM state. Increase the total number of and register windows—and a call might overflow ei- registers in your VM state. And add support for a fi- ther the call stack or the register file. For lab, don’t 1 nite stack of activation records. Sizes are up to you, worry about overflow—you’ll deal with it in step (12).

1 It is possible to allocate procedure activations dynamically and to In the implementation of the call instruction, I recom- keep running until malloc fails, but this plan is terrible, on two grounds: mend you define these local C variables: (1) It makes calls inefficient. (2) If a user runs a VM program that has an infinite , it’s not sensible for the SVM to run until destreg The number of the register that is meant to it gobbles up as much address space as the will allow it—the sensible response is to cap the size of the call stack, and receive the result of the call. For the call instruc- when the call stack overflows, to issue an informative error message. tion this is the X field.

3 funreg The number of the register that holds the func- returnt is rigged so that if the return doesn’t re- tion being called. For the call instruction this is store the proper program counter, code will hit the Y field. some error instructions.

lastarg The number of the register that holds the last D. See if a value can be returned correctly. Test re- argument (or in the case where there are no argu- turnv tests that value 1852 is correctly returned ments, again the function being called). For the from a function. call instruction this is the Z field. E. Test recursion. Test fact recursively computes Then write your code in terms of these variables. 5 factorial. Test your call instruction using the testing handout: A. See if control is transferred: callt calls a function After lab that runs check and expect, then halts. Tail calls B. See if an argument can be passed and the register window works correctly. The callarg test code If you want a super-quick review of tail calls, you could do puts a function in register 200 and an argument worse than my lecture notes from COMP 105. in register 201. For the test to pass, the callee must see the actual parameter in register 1. (11) Implement tail call. Implement the tailcall instruc- tion. Informally, this instruction should update the Don’t overlook the run-time disassembler built instruction stream, use memmove to shift register values into the SVM. As mentioned in step (5), your vm- into place, and start executing the new function from run function should have the option of calling idump instruction 0. A tail call can’t overflow the call stack, after every instruction is fetched. This feature is but it can overflow the register file—but for lab, don’t great for debugging, and if you followed the advice worry about it. For a precise specification, con- in step (5), yours should be set up to be enabled by sult the operational semantics. running env SVMDEBUG=decode svm. A caution: beware of trying to implement call by A tail call has no destreg, but you can and should making a recursive call to vmrun. It can be made define local variables funreg and lastarg, which for a to work, but there are risks: tail call are instruction fields X and Y. If you have used these variables wisely, you can copy, paste, and • I hope you’ll exit the course with a strong grasp edit code from your call instruction in step (9). of how recursion works. But if you rely on C recursion instead of implementing it yourself, you The registers that have to be shifted are the function might not grasp it as well. register and all the arguments: registers funreg to las- • You won’t be able to implement tail calls this way. targ, inclusive. Be aware of the possibility of an off- • You won’t be able to spit out a on by-one error. error. • When we get to garbage collection in modules Once your tailcall is implemented, test it using the 9 and 10, you’ll be screwed and you’ll have to testing handout: do the calls over. F. Test tail makes a tailcall r0(r1), so it has (10) Implement return. Implement the return instruction. a chance of working even if the registers aren’t Informally, this instruction should grab the return moved. The test counts down from 555,000 by value, pop the caller’s activation off the stack,2 then ones, making a tail call at each step. The uscheme restore the instruction stream, program counter, and and vscheme interpreters can’t run this test, be- register window. It should then set the destination reg- cause they don’t optimize tail calls. The uscheme- ister and continue executing in the caller’s code. For plus interpreter can run it. a precise specification, consult the operational semantics. Run this test using valgrind, which may detect if you have a bug that makes your call stack over- Once your return is implemented, test it using the test- flow. ing handout: G. Test tailm uses tail calls to add 12 to 99, 1.2 mil- C. See if control is transferred there and back, and lion times. This one requires that the registers be if the register window is restored correctly. Test shifted into place correctly. The test runs over 10 2Or if the stack is empty, a checked run-time error times faster than the same test in uschemeplus.

4 Stack overflow choose one language feature and will propose how that feature would be implemented by the UFT and SVM. (12) Detect overflow. The SVM has two data structures This week, you’ll propose three candidate features. that can overflow: Each feature must be characterized in two ways: • A call instruction can overflow the call stack by attempting to push an activation when the stack • A brief description of the feature is full. This situation should be handled by a call • The name of a language in which the feature is found to runerror.

A tailcall instruction does not push an activa- Here are some examples: tion record, so it cannot overflow the call stack. • Pattern matching as in Erlang • A call instruction can overflow the register file by • Exception handlers as in Python attempting to shift the current window in such a • Variable number of arguments as in way that not all register names (0 to 255) point to • as in Lua correctly allocated memory. This situation should • Goal-directed evaluation as in Icon also be handled by a call to runerror. • String scanning as in Icon • Goroutines and channels as in Go A tailcall can overflow the register file. • Generators as in Python Extend your call and tailcall instructions so • Named and optional arguments as in OCaml they detect overflow. • Implicit parameters as in Haskell • Prototype-based objects as in Self (or Lua or There are two ways to cause overflow: run out of regis- JavaScript) ters while still having plenty of activation records, and run out of activation records while still having plenty of Feel free to pick from these examples or to pick your registers. Demonstrate your understanding with these own. A search for “interesting programming-language test files: features” might also turn up something interesting.

overstack.vs Contains a program that, when exe- cuted, overflows the call stack of your SVM. What and how to submit (A good way to do this is with a deep recursive call.) (14) On Sunday, submit the homework. In the src/uft di- rectory you’ll find a file SUBMIT.07. That file needs to overreg.vs Contains a program that, when executed, be edited to answer the same questions you answer ev- does not overflow the call stack of your SVM, but ery week, plus your three proposed features from does overflow the register file. It is sufficient if step (13). the register file overflows before the call stack. (A good way to do this is to adapt the previ- To submit, you’ll need to copy your working tree to ous program so the recursive function uses reg- the department servers. We recommend using rsync, ister 255, which will then eat up 256 registers per but scp also works. call.) Now log into a department server, change to your work- overtail.vs Contains a program that is as similar as ing tree, and submit your entire src directory: possible to the program in overstack.vs, but in which the recursive call is a tailcall. This pro- provide comp150vm hw07 src gram should run to completion without error. (15) On Monday, submit your reflection. Create a plain text Application to other languages file REFLECTION, which will hold your claims for project points and depth points. When you finish the course, I hope you will be confident that you can apply your new skills to other languages be- For each project point you claim, write the number sides vScheme. You will demonstrate those skills in the of the point, plus whatever is called for in the section course finale, and next week you and your classmates will “How to claim the project points”—usually a few sen- have a chance to practice. To give you material to prac- tences. tice on, this week you’ll pick out three language features that might be worth studying. Now copy your REFLECTION file to a department server and submit it using provide: (13) Identify three language features that might be worth studying. Next week, you and your classmates will provide comp150vm reflection07 REFLECTION

5 Overview of the code trace useful, you extend the representation of FUNCODE to include a string that encodes the function’s name We’re back to the SVM! Adding support for calls will extend (if known) and source-code location (if known). The ex- VM state and will add support for the three instructions. tension is supported by your K-normal form, assembly code (including parser), , SVM loader, and New code you will write or edit vmrun. vmstack.h You’ll define the contents of an activation 10. Error checking and recovery [4 points]. You implement record. a check-error instruction, which works by putting a special frame on the call stack. If that frame is re- Code you will extend turned to by normal execution, the check-error test fails. But if an error occurs while that frame is on the vmstate.h You’ll add a call stack to the VM state, and stack, the test succeeds. you’ll enlarge the register file. 11. Code improvements for tail calls, part I: Register tar- opcodes.h You’ll add internal opcodes for call, tail-call, and geting. [3 points] You define an optimization pass for return instructions. K-normal form which changes the register bindings so that every tail call is made to a function in register 0 instructions.c To make the call, tail-call, and return in- with arguments in registers 1 to 푁. This is done not structions known to the loader, you’ll add them to the by adding new bindings (which the VM can to more instruction table. efficiently) but by changing the registers used in exist- vmrun.c You’ll add the run-time behavior of the call, tail- ing bindings. Such a renaming is always possible, and call, and return instructions. the cost can be limited to at most a single new VM instruction. Learning outcomes In native code, this optimization is important. In a VM, its payoff is small. But it enables the next Outcomes available for points optimization, which has high payoff in both settings. You can claim a project point for each of the learning out- 12. Code improvements for tail calls, part II: Optimized comes listed here. Instructions about how to claim each tail recursion [2 points]. Building on the previous step, point are found below. your UFT recognizes when a tail call is a recursive call, and it optimizes that tail call into a instruction. 1. Operational semantics. You understand the opera- (A good heuristic for recognizing that a tail call is re- tional semantics of register windows. cursive is that it calls a function in register 0.) 2. Operational semantics. You understand the opera- This optimization is essential for functional languages: tional semantics of the return instruction. a tail-recursive function is optimized into a loop. 3. Overflow. You can build an SVM that detects stack overflow, preventing memory errors. 13. Curried functions [5 points]. You implement automatic currying as described in the handout on understanding 4. Optimized tail calls. You can use tail calls to write a procedure calls. deeply recursive function that avoids overflow. The strategy I recommend is called “eval/apply” and is 5. Invariants, part I. You know the invariants satisfied by described by Simon Marlow and Simon Peyton Jones in your representation of a VM function. a landmark 2006 paper. The paper has way more detail 6. Invariants, part II. You know how VM-function invari- than you need, but the important bits will involve what ants are used in your vmrun function. new species of value and what new species of activation record are useful for implementing curried functions. 7. Performance of register windows. You understand the benefits and costs of register windows. 14. Exceptions [5 points]. Implement exceptions and excep- tion handlers. I recommend defining two VM instruc- 8. Language features. Your Sunday night submission in- tions that resemble the “long label” and “long goto” cludes three proposed language features. features from chapter 3 of my book, which I can give You can claim depth points for doing things with the call you. Then build exceptions on top of that. stack. 15. Coroutines [10 points]. A is a synchronous, 9. Stack trace [3 points]. When a run-time error occurs, cooperative, lightweight, concurrent abstraction. It’s your code produces a stack trace. To make the stack like a , only simpler. A coroutine is represented

6 by a suspended VM state. Coroutines need at least the • Explain what, if any, is the performance benefit following: of using register windows in the SVM, as opposed to a fixed set of 256 VM registers. • A new form of value whose payload is a VM state. • Explain what, if any, is the performance cost (in • Instructions that create a coroutine (from a func- your VM code) of using register windows in the tion) and wait for one to terminate. SVM, as opposed to a fixed set of 256 VM regis- ters. • Instructions that transfer control between corou- tines. These can be symmetric (as in Icon) or 8. To claim this point, include three features in your SUB- asymmetric (as in Lua). MIT.07 file on Sunday. Coroutines are a good first step toward fully preemp- tive concurrency with synchronous communication, as in Erlang or Go.

How to claim the project points Each of the numbered learning outcomes 1 to 8 is worth one point, and the points can be claimed as follows: 1. To claim this point, give us the number of the line of code in file vmrun.c that implements 푅 ⊕ 푟0 in the rule for call. 2. To claim this point, give us the number of the line of code in file vmrun.c that computes 휎(푅(푟)) and the line of code that updates location 푅′(푟′) in 휎.

3. To claim this point, submit files overstack.vs and overreg.vs from step (12), and confirm that they as- semble, load, and detect overflow by the following com- mands:

uft vs-vo overstack.vs | valgrind svm uft vs-vo overreg.vs | valgrind svm Confirm that

• Each command causes the svm to terminate with a sensible error message. • The error messages are different in the two cases.

• No memory errors are detected by valgrind.

4. To claim this point, submit the overtail.vs file from step (12), along with a brief explanation—one or two sentences at most—of its similarities to and differences from overstack.vs. 5. To claim this point, identify one important invariant that the representation of struct VMFunction must sat- isfy. (Hint: If any information is redundant, it must be internally consistent.) 6. To claim this point, explain how the invariant in the previous step is exploited in the implementation of vm- run. 7. To claim this point, answer these two questions:

7