1 00:00:00,229 --> 00:00:05,140 One of the most useful abstractions provided by high-level languages is the notion of a 2 00:00:05,140 --> 00:00:10,169 procedure or subroutine, which is a sequence of instructions that perform a specific task. 3 00:00:10,169 --> 00:00:15,210 A procedure has a single named entry point, which can be used to refer to the procedure 4 00:00:15,210 --> 00:00:17,190 in other parts of the program. 5 00:00:17,190 --> 00:00:22,650 In the example here, this code is defining the GCD procedure, which is declared to return 6 00:00:22,650 --> 00:00:25,810 an integer value. 7 00:00:25,810 --> 00:00:30,689 Procedures have zero or more formal parameters, which are the names the code inside the procedure 8 00:00:30,689 --> 00:00:34,772 will use to refer the values supplied when the procedure is invoked by a "procedure call". 9 00:00:34,772 --> 00:00:41,000 A procedure call is an expression that has the name of the procedure followed by parenthesized 10 00:00:41,000 --> 00:00:46,809 list of values called "arguments" that will be matched up with the formal parameters. 11 00:00:46,809 --> 00:00:50,990 For example, the value of the first argument will become the value of the first formal 12 00:00:50,990 --> 00:00:53,519 parameter while the procedure is executing. 13 00:00:53,519 --> 00:00:57,840 The body of the procedure may define additional variables, called "local variables", since 14 00:00:57,840 --> 00:01:02,019 they can only be accessed by statements in the procedure body. 15 00:01:02,019 --> 00:01:07,790 Conceptually, the storage for local variables only exists while the procedure is executing. 16 00:01:07,790 --> 00:01:12,890 They are allocated when the procedure is invoked and deallocated when the procedure returns. 17 00:01:12,890 --> 00:01:17,750 The procedure may return a value that's the result of the procedure's computation. 18 00:01:17,750 --> 00:01:22,220 It's legal to have procedures that do not return a value, in which case the procedures 19 00:01:22,220 --> 00:01:27,500 would only be executed for their "side effects", e.g., changes they make to shared data. 20 00:01:27,500 --> 00:01:32,860 Here we see another procedure, COPRIMES, that invokes the GCD procedure to compute the greatest 21 00:01:32,860 --> 00:01:36,130 common divisor of two numbers. 22 00:01:36,130 --> 00:01:43,060 To use GCD, the programmer of COPRIMES only needed to know the input/output behavior of 23 00:01:43,060 --> 00:01:48,280 GCD, i.e., the number and types of the arguments and what type of value is returned as a result. 24 00:01:48,280 --> 00:01:52,640 The procedural abstraction has hidden the implementation of GCD, while still making 25 00:01:52,640 --> 00:01:56,590 its functionality available as a "black box". 26 00:01:56,590 --> 00:02:02,320 This is a very powerful idea: encapsulating a complex computation so that it can be used 27 00:02:02,320 --> 00:02:04,110 by others. 28 00:02:04,110 --> 00:02:09,410 Every high-level language comes with a collection of pre-built procedures, called "libraries", 29 00:02:09,410 --> 00:02:14,319 which can be used to perform arithmetic functions (e.g., square root or cosine), 30 00:02:14,319 --> 00:02:21,500 manipulate collections of data (e.g., lists or dictionaries), read data from files, and 31 00:02:21,500 --> 00:02:24,590 so on - the list is nearly endless! 32 00:02:24,590 --> 00:02:29,019 Much of the expressive power and ease-of-use provided by high-level languages comes from 33 00:02:29,019 --> 00:02:32,609 their libraries of "black boxes". 34 00:02:32,609 --> 00:02:37,540 The procedural abstraction is at the heart of object-oriented languages, which encapsulate 35 00:02:37,540 --> 00:02:42,999 data and procedures as black boxes called objects that support specific operations on 36 00:02:42,999 --> 00:02:44,920 their internal data. 37 00:02:44,920 --> 00:02:50,909 For example, a LIST object has procedures (called "methods" in this context) for indexing 38 00:02:50,909 --> 00:02:56,340 into the list to read or change a value, adding new elements to the list, inquiring about 39 00:02:56,340 --> 00:02:58,909 the length of the list, and so on. 40 00:02:58,909 --> 00:03:03,569 The internal representation of the data and the algorithms used to implement the methods 41 00:03:03,569 --> 00:03:06,500 are hidden by the object abstraction. 42 00:03:06,500 --> 00:03:10,959 Indeed, there may be several different LIST implementations to choose from depending on 43 00:03:10,959 --> 00:03:14,349 which operations you need to be particularly efficient. 44 00:03:14,349 --> 00:03:18,909 Okay, enough about the virtues of the procedural abstraction! 45 00:03:18,909 --> 00:03:23,959 Let's turn our attention to how to implement procedures using the Beta ISA. 46 00:03:23,959 --> 00:03:29,049 A possible implementation is to "inline" the procedure, where we replace the procedure 47 00:03:29,049 --> 00:03:34,049 call with a copy of the statements in the procedure's body, substituting argument values 48 00:03:34,049 --> 00:03:36,819 for references to the formal parameters. 49 00:03:36,819 --> 00:03:42,379 In this approach we're treating procedures very much like UASM macros, i.e., a simple 50 00:03:42,379 --> 00:03:46,579 notational shorthand for making a copy of the procedure's body. 51 00:03:46,579 --> 00:03:49,860 Are there any problems with this approach? 52 00:03:49,860 --> 00:03:53,819 One obvious issue is the potential increase in the code size. 53 00:03:53,819 --> 00:03:59,349 For example, if we had a lengthy procedure that was called many times, the final expanded 54 00:03:59,349 --> 00:04:01,609 code would be huge! 55 00:04:01,609 --> 00:04:06,769 Enough so that inlining isn't a practical solution except in the case of short procedures 56 00:04:06,769 --> 00:04:11,540 where optimizing compilers do sometimes decide to inline the code. 57 00:04:11,540 --> 00:04:18,089 A bigger difficulty is apparent when we consider a recursive procedure where there's a nested 58 00:04:18,089 --> 00:04:20,690 call to the procedure itself. 59 00:04:20,690 --> 00:04:25,530 During execution the recursion will terminate for some values of the arguments and the recursive 60 00:04:25,530 --> 00:04:28,170 procedure will eventually return answer. 61 00:04:28,170 --> 00:04:34,670 But at compile time, the inlining process would not terminate and so the inlining scheme 62 00:04:34,670 --> 00:04:38,510 fails if the language allows recursion. 63 00:04:38,510 --> 00:04:41,380 The second option is to "link" to the procedure. 64 00:04:41,380 --> 00:04:45,230 In this approach there is a single copy of the procedure code which we arrange to be 65 00:04:45,230 --> 00:04:52,530 run for each procedure call - all the procedure calls are said to link to the procedure code. 66 00:04:52,530 --> 00:04:56,920 Here the body of the procedure is translated once into Beta instructions and the first 67 00:04:56,920 --> 00:05:00,410 instruction is identified as the procedure's entry point. 68 00:05:00,410 --> 00:05:04,190 The procedure call is compiled into a set of instructions that evaluate the argument 69 00:05:04,190 --> 00:05:08,720 expressions and save the values in an agreed-upon location. 70 00:05:08,720 --> 00:05:14,220 Then we'll use a BR instruction to transfer control to the entry point of the procedure. 71 00:05:14,220 --> 00:05:20,160 Recall that the BR instruction not only changes the PC but saves the address of the instruction 72 00:05:20,160 --> 00:05:23,690 following the branch in a specified register. 73 00:05:23,690 --> 00:05:28,760 This saved address is the "return address" where we want execution to resume when procedure 74 00:05:28,760 --> 00:05:31,680 execution is complete. 75 00:05:31,680 --> 00:05:36,490 After branching to the entry point, the procedure code runs, stores the result in an agreed-upon 76 00:05:36,490 --> 00:05:41,390 location and then resumes execution of the calling program by jumping to the supplied 77 00:05:41,390 --> 00:05:44,000 return address. 78 00:05:44,000 --> 00:05:48,830 To complete this implementation plan we need a "calling convention" that specifies where 79 00:05:48,830 --> 00:05:52,900 to store the argument values during procedure calls and where the procedure should store 80 00:05:52,900 --> 00:05:54,300 the return value. 81 00:05:54,300 --> 00:05:59,220 It's tempting to simply allocate specific memory locations for the job. 82 00:05:59,220 --> 00:06:01,360 How about using registers? 83 00:06:01,360 --> 00:06:06,210 We could pass the argument value in registers starting, say, with R1. 84 00:06:06,210 --> 00:06:10,460 The return address could be stored in another register, say R28. 85 00:06:10,460 --> 00:06:14,900 As we can see, with this convention the BR and JMP instructions are just what we need 86 00:06:14,900 --> 00:06:17,490 to implement procedure call and return. 87 00:06:17,490 --> 00:06:22,060 It's usual to call the register holding the return address the "linkage pointer". 88 00:06:22,060 --> 00:06:27,180 And finally the procedure can use, say, R0 to hold the return value. 89 00:06:27,180 --> 00:06:30,550 Let's see how this would work when executing the procedure call fact(3). 90 00:06:30,550 --> 00:06:36,810 As shown on the right, fact(3) requires a recursive call to compute fact(2), and so 91 00:06:36,810 --> 00:06:37,810 on. 92 00:06:37,810 --> 00:06:43,440 Our goal is to have a uniform calling convention where all procedure calls and procedure bodies 93 00:06:43,440 --> 00:06:48,100 use the same convention for storing arguments, return addresses and return values. 94 00:06:48,100 --> 00:06:54,740 In particular, we'll use the same convention when compiling the recursive call fact(n-1) 95 00:06:54,740 --> 00:06:57,780 as we did for the initial call to fact(3). 96 00:06:57,780 --> 00:06:59,220 Okay. 97 00:06:59,220 --> 00:07:03,240 In the code shown on the right we've used our proposed convention when compiling the 98 00:07:03,240 --> 00:07:05,210 Beta code for fact(). 99 00:07:05,210 --> 00:07:07,580 Let's take a quick tour. 100 00:07:07,580 --> 00:07:13,410 To compile the initial call fact(3) the compiler generated a CMOVE instruction to put the argument 101 00:07:13,410 --> 00:07:19,470 value in R1 and then a BR instruction to transfer control to fact's entry point while remembering 102 00:07:19,470 --> 00:07:22,890 the return address in R28. 103 00:07:22,890 --> 00:07:29,461 The first statement in the body of fact tests the value of the argument using CMPLEC and 104 00:07:29,461 --> 00:07:32,290 BT instructions. 105 00:07:32,290 --> 00:07:37,550 When n is greater than 0, the code performs a recursive call to fact, saving the value 106 00:07:37,550 --> 00:07:43,960 of the recursive argument n-1 in R1 as our convention requires. 107 00:07:43,960 --> 00:07:47,680 Note that we had to first save the value of the original argument n because we'll need 108 00:07:47,680 --> 00:07:54,220 it for the multiplication after the recursive call returns its value in R0. 109 00:07:54,220 --> 00:07:59,720 If n is not greater than 0, the value 1 is placed in R0. 110 00:07:59,720 --> 00:08:05,030 Then the two possible execution paths merge, each having generated the appropriate return 111 00:08:05,030 --> 00:08:10,770 value in R0, and finally there's a JMP to return control to the caller. 112 00:08:10,770 --> 00:08:15,990 The JMP instruction knows to find the return address in R28, just where the BR put it as 113 00:08:15,990 --> 00:08:20,460 part of the original procedure call. 114 00:08:20,460 --> 00:08:25,830 Some of you may have noticed that there are some difficulties with this particular implementation. 115 00:08:25,830 --> 00:08:30,420 The code is correct in the sense that it faithfully implements procedure call and return using 116 00:08:30,420 --> 00:08:32,969 our proposed convention. 117 00:08:32,969 --> 00:08:37,860 The problem is that during recursive calls we'll be overwriting register values we need 118 00:08:37,860 --> 00:08:39,860 later. 119 00:08:39,860 --> 00:08:46,230 For example, note that following our calling convention, the recursive call also uses R28 120 00:08:46,230 --> 00:08:47,959 to store the return address. 121 00:08:47,959 --> 00:08:52,670 When executed, the code for the original call stored the address of the HALT instruction 122 00:08:52,670 --> 00:08:54,730 in R28. 123 00:08:54,730 --> 00:08:59,730 Inside the procedure, the recursive call will store the address of the MUL instruction in 124 00:08:59,730 --> 00:09:01,060 R28. 125 00:09:01,060 --> 00:09:05,110 Unfortunately that overwrites the original return address. 126 00:09:05,110 --> 00:09:10,610 Even the attempt to save the value of the argument N in R2 is doomed to fail since during 127 00:09:10,610 --> 00:09:15,910 the execution of the recursive call R2 will be overwritten. 128 00:09:15,910 --> 00:09:20,879 The crux of the problem is that each recursive call needs to remember the value of its argument 129 00:09:20,879 --> 00:09:27,810 and return address, i.e., we need two storage locations for each active call to fact(). 130 00:09:27,810 --> 00:09:33,939 And while executing fact(3), when we finally get to calling fact(0) there are four nested 131 00:09:33,939 --> 00:09:38,649 active calls, so we'll need 4*2 = 8 storage locations. 132 00:09:38,649 --> 00:09:44,860 In fact, the amount of storage needed varies with the depth of the recursion. 133 00:09:44,860 --> 00:09:49,889 Obviously we can't use just two registers (R2 and R28) to hold all the values we need 134 00:09:49,889 --> 00:09:51,949 to save. 135 00:09:51,949 --> 00:09:54,259 One fix is to disallow recursion! 136 00:09:54,259 --> 00:10:00,050 And, in fact, some of the early programming languages such as FORTRAN did just that. 137 00:10:00,050 --> 00:10:02,429 But let's see if we can solve the problem another way.