3

I'm trying to implement functions and recursion in an ASM-like simplified language that has no procedures. Only simple jumpz, jump, push, pop, add, mul type commands.

Here are the commands:
(all variables and literals are integers)

  • set (sets the value of an already existing variable or declares and initializes a new variable) e.g. (set x 3)
  • push (pushes a value onto the stack. can be a variable or an integer) e.g. (push 3) or (push x)
  • pop (pops the stack into a variable) e.g. (pop x)
  • add (adds the second argument to the first argument) e.g. (add x 1) or (add x y)
  • mul (same as add but for multiplication)
  • jump (jumps to a specific line of code) e.g. (jump 3) would jump to line 3 or (jump x) would jump to the line # equal to the value of x
  • jumpz (jumps to a line number if the second argument is equal to zero) e.g. (jumpz 3 x) or (jumpz z x)

The variable 'IP' is the program counter and is equal to the line number of the current line of code being executed.

In this language, functions are blocks of code at the bottom of the program that are terminated by popping a value off the stack and jumping to that value. (using the stack as a call stack) Then the functions can be called anywhere else in the program by simply pushing the instruction pointer onto the stack and then jumping to the start of the function.

This works fine for non-recursive functions.

How could this be modified to handle recursion?

I've read that implementing recursion with a stack is a matter of pushing parameters and local variables onto the stack (and in this lower level case, also the instruction pointer I think)

I wouldn't be able to do something like x = f(n). To do this I'd have some variable y (that is also used in the body of f), set y equal to n, call f which assigns its "return value" to y and then jumps control back to where f was called from, where we then set x equal to y.

(a function that squares a number whose definition starts at line 36)

1 - set y 3
2 - set returnLine IP
3 - add returnLine 2
4 - push returnLine
5 - jump 36
6 - set x y
...
36 - mul y 2
37 - pop returnLine
38 - jump returnLine

This doesn't seem to lend itself to recursion. Arguments and intermediate values would need to go on the stack and I think multiple instances on the stack of the same address would result from recursive calls which is fine.

John Smith
  • 95
  • 5

3 Answers3

1

Next code raises the number "base" to the power "exponent" recursively in "John Smith Assembly":

1 - set base 2            ;RAISE 2 TO ...
2 - set exponent 4        ;... EXPONENT 4 (2^4=16).
3 - set result 1          ;MUST BE 1 IN ORDER TO MULTIPLY.
4 - set returnLine IP     ;IP = 4.
5 - add returnLine 4      ;RETURNLINE = 4+4.
6 - push returnLine       ;PUSH 8.
7 - jump 36               ;CALL FUNCTION.
.
.
.
;POWER FUNCTION.
36 - jumpz 43 exponent    ;FINISH IF EXPONENT IS ZERO.

37 - mul result base      ;RESULT = ( RESULT * BASE ).
38 - add exponent -1      ;RECURSIVE CONTROL VARIABLE.
39 - set returnLine IP    ;IP = 39.
40 - add returnLine 4     ;RETURN LINE = 39+4.
41 - push returnLine      ;PUSH 43.
42 - jump 36              ;RECURSIVE CALL.

43 - pop returnLine
44 - jump returnLine
;POWER END.

In order to test it, let's run it manually :

 LINE | BASE EXPONENT RESULT RETURNLINE STACK
------|---------------------------------------
   1  |   2
   2  |         4
   3  |                  1
   4  |                           4
   5  |                           8
   6  |                                   8
   7  |
  36  |
  37  |                  2
  38  |         3
  39  |                          39
  40  |                          43
  41  |                                  43(1)
  42  |
  36  |
  37  |                  4
  38  |         2
  39  |                          39
  40  |                          43
  41  |                                  43(2)
  42  |
  36  |
  37  |                  8
  38  |         1
  39  |                         39
  40  |                         43
  41  |                                  43(3)
  42  |
  36  |
  37  |                 16
  38  |         0
  39  |                         39
  40  |                         43
  41  |                                  43(4)
  42  |
  36  |
  43  |                         43(4)
  44  |
  43  |                         43(3)
  44  |
  43  |                         43(2)
  44  |
  43  |                         43(1)
  44  |
  43  |                          8
  44  |
   8  |

Edit : parameter for function now on stack (didn't run it manually) :

1 - set base 2            ;RAISE 2 TO ...
2 - set exponent 4        ;... EXPONENT 4 (2^4=16).
3 - set result 1          ;MUST BE 1 IN ORDER TO MULTIPLY.
4 - set returnLine IP     ;IP = 4.
5 - add returnLine 7      ;RETURNLINE = 4+7.
6 - push returnLine       ;PUSH 11.
7 - push base             ;FIRST PARAMETER.
8 - push result           ;SECOND PARAMETER.
9 - push exponent         ;THIRD PARAMETER.
10 - jump 36              ;FUNCTION CALL.
...
;POWER FUNCTION.
36 - pop exponent         ;THIRD PARAMETER.
37 - pop result           ;SECOND PARAMETER.
38 - pop base             ;FIRST PARAMETER.
39 - jumpz 49 exponent    ;FINISH IF EXPONENT IS ZERO.

40 - mul result base      ;RESULT = ( RESULT * BASE ).
41 - add exponent -1      ;RECURSIVE CONTROL VARIABLE.
42 - set returnLine IP    ;IP = 42.
43 - add returnLine 7     ;RETURN LINE = 42+7.
44 - push returnLine      ;PUSH 49.
45 - push base
46 - push result
47 - push exponent
48 - jump 36              ;RECURSIVE CALL.


49 - pop returnLine
50 - jump returnLine
;POWER END.
  • Isn't this basically iterating though? It's doing more of a "repeatedly multiply by two" than a "multiply two by the result of 2^(n-1)" I think. I could be way off though – John Smith Aug 05 '16 at 16:11
  • I'm thinking of something where when you get to the first 2 * f(n-1), you put the 2 on the stack.. and maybe the n-1 on the stack.. as well as the address. But it's so hard for me to wrap my head around how to get the recursion to "recurse" with this primitive language – John Smith Aug 05 '16 at 16:14
  • I think that might be what I'm looking for here – John Smith Aug 05 '16 at 16:17
  • 1
    @JohnSmith Do you mean something like [this](http://pastebin.com/3g43MzWA)? – Margaret Bloom Aug 05 '16 at 16:32
  • Jose, I think your parameter-pushing code is probably exactly what I'm looking for. I need to study it for a minute to understand it though before I mark as answer – John Smith Aug 05 '16 at 16:48
  • @JohnSmith, now you have to decide one last thing : what to do with the final result. This must be done in line 49, before "result" is lost. You can store it in another variable, like `49 - set x result` , `50 - pop returnLine`, or you can push it on stack, like `49 - pop returnLine` , `50 - push result`. – Jose Manuel Abarca Rodríguez Aug 05 '16 at 17:26
  • I'm confused. Why would result be lost? My plan was just to have 11 - print result – John Smith Aug 05 '16 at 17:39
  • Oh, there is a "print" instruction, I like it : `11 - print result`. – Jose Manuel Abarca Rodríguez Aug 05 '16 at 17:43
  • 1
    Thanks. I kind of made this language up as I went along. I understand your solution now. It's definitely 100% what I'm looking for. I just wrote an interpreter for the language in C# and put your code in and a nice little 16 popped out. – John Smith Aug 05 '16 at 17:51
1

Your asm does provide enough facilities to implement the usual procedure call / return sequence. You can push a return address and jump as a call, and pop a return address (into a scratch location) and do an indirect jump to it as a ret. We can just make call and ret macros. (Except that generating the correct return address is tricky in a macro; we might need a label (push ret_addr), or something like set tmp, IP / add tmp, 4 / push tmp / jump target_function). In short, it's possible and we should wrap it up in some syntactic sugar so we don't get bogged down with that while looking at recursion.

With the right syntactic sugar, you can implement Fibonacci(n) in assembly that will actually assemble for both x86 and your toy machine.


You're thinking in terms of functions that modify static (global) variables. Recursion requires local variables so each nested call to the function has its own copy of local variables. Instead of having registers, your machine has (apparently unlimited) named static variables (like x and y). If you want to program it like MIPS or x86, and copy an existing calling convention, just use some named variables like eax, ebx, ..., or r0 .. r31 the way a register architecture uses registers.

Then you implement recursion the same way you do in a normal calling convention, where either the caller or callee use push / pop to save/restore a register on the stack so it can be reused. Function return values go in a register. Function args should go in registers. An ugly alternative would be to push them after the return address (creating a caller-cleans-the-args-from-the-stack calling convention), because you don't have a stack-relative addressing mode to access them the way x86 does (above the return address on the stack). Or you could pass return addresses in a link register, like most RISC call instructions (usually called bl or similar, for branch-and-link), instead of pushing it like x86's call. (So non-leaf callees have to push the incoming lr onto the stack themselves before making another call)


A (silly and slow) naively-implemented recursive Fibonacci might do something like:

int Fib(int n) {
    if(n<=1) return n;          // Fib(0) = 0;  Fib(1) = 1
    return Fib(n-1) + Fib(n-2);
}

## valid implementation in your toy language *and* x86 (AMD64 System V calling convention)

### Convenience macros for the toy asm implementation
# pretend that the call implementation has some way to make each return_address label unique so you can use it multiple times.
# i.e. just pretend that pushing a return address and jumping is a solved problem, however you want to solve it.
%define call(target)   push return_address; jump target; return_address:
%define ret            pop rettmp; jump rettmp    # dedicate a whole variable just for ret, because we can
# As the first thing in your program,  set eax, 0  / set ebx, 0 / ...

global Fib
Fib:
    # input: n in edi.
    # output: return value in eax
      # if (n<=1) return n;  // the asm implementation of this part isn't interesting or relevant.  We know it's possible with some adds and jumps, so just pseudocode / handwave it:
    ... set eax, edi and ret  if edi <= 1 ... # (not shown because not interesting)
    add     edi, -1
    push    edi        # save n-1 for use after the recursive call
    call    Fib        # eax = Fib(n-1)
    pop     edi        # restore edi to *our* n-1
    push    eax        # save the Fib(n-1) result across the call
    add     edi, -1
    call    Fib        # eax = Fib(n-2)
    pop     edi        # use edi as a scratch register to hold Fib(n-1) that we saved earlier
    add     eax, edi   # eax = return value = Fib(n-1) + Fib(n-2)
    ret

During a recursive call to Fib(n-1) (with n-1 in edi as the first argument), the n-1 arg is also saved on the stack, to be restored later. So each function's stack frame contains the state that needs to survive the recursive call, and a return address. This is exactly what recursion is all about on a machine with a stack.

Jose's example doesn't demonstrate this as well, IMO, because no state needs to survive the call for pow. So it just ends up pushing a return address and args, then popping the args, building up just some return addresses. Then at the end, follows the chain of return addresses. It could be extended to save local state across each nested call, doesn't actually illustrate it.


My implementation is a bit different from how gcc compiles the same C function for x86-64 (using the same calling convention of first arg in edi, ret value in eax). gcc6.1 with -O1 keeps it simple and actually does two recursive calls, as you can see on the Godbolt compiler explorer. (-O2 and especially -O3 do some aggressive transformations). gcc saves/restores rbx across the whole function, and keeps n in ebx so it's available after the Fib(n-1) call. (and keeps Fib(n-1) in ebx to survive the second call). The System V calling convention specifies rbx as a call-preserved register, but rbi as call-clobbered (and used for arg-passing).


Obviously you can implement Fib(n) much faster non-recursively, with O(n) time complexity and O(1) space complexity, instead of O(Fib(n)) time and space (stack usage) complexity. It makes a terrible example, but it is trivial.

Community
  • 1
  • 1
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • "An ugly alternative", you hurt my feelings :) – Jose Manuel Abarca Rodríguez Aug 05 '16 at 18:33
  • @JoseManuelAbarcaRodríguez: I hadn't really grokked your code until after I finished my answer. I looked at it, but it seemed over-complicated. It was only later I realized that's what you were doing. I think stack args are a mistake for a machine with unlimited named registers, as long as it can't be multi-threaded. It works, but I found it harder to convince myself that it was truly recursive rather than just a loop that spilled/reloaded. Your function eats up the args from the stack, leaving only the chain of return addresses, so each stack frame doesn't have any saved locals. – Peter Cordes Aug 05 '16 at 18:44
  • I think things like "call Fib" go outside what my language can easily do – John Smith Aug 05 '16 at 18:52
  • @JohnSmith: read the macro definitions a few lines up. It's just short-hand for `push IP` / `jump Fib` to keep the code readable. Same for `ret`: it pops into a temporary and does an indirect jump. – Peter Cordes Aug 05 '16 at 18:53
  • 1
    Oh I see. I'm stepping through it to try to understand – John Smith Aug 05 '16 at 19:04
  • @JohnSmith: I just realized that `push IP` isn't right; it needs to push the return address, i.e. the next instruction after the `jump`. Still, we definitely want to hide all that behind a macro so we can think about it like the x86 `call` instruction and look at the rest of the code. I made an edit that has a clumsy label at the end of the macro, or you could also `set rettmp, IP / add rettmp, 4 / push rettmp / jump target` if `push` can't push the address of a label. – Peter Cordes Aug 05 '16 at 19:09
  • when you say " ... return 1" Do you mean to store 1 in eax and do a 'ret'? – John Smith Aug 05 '16 at 19:15
  • @JohnSmith: yes, exactly. I thought it would clutter the function to much to actually implement it with jumpz (and I didn't want to bother implementing that part :). Anyway, it's obviously possible with `set` / `add` and `jumpz` given some scratch space. Oops, that's not even the right recurrence relation for Fib(n). `Fib(0) = 0`, so I updated the code to be correct and more specific. – Peter Cordes Aug 05 '16 at 19:21
  • " set eax, edi and ret if edi <= 1" what does edi need to be set to here? I would have thought setting eax to 1 and returning would be enough. or maybe I'm misunderstanding what you meant here – John Smith Aug 05 '16 at 19:32
  • @JohnSmith: `edi` at that point holds `n`, the argument to that nested call. So `set eax, edi` implements the `return n` part of the C. – Peter Cordes Aug 05 '16 at 19:33
  • Ahh I see. I was misreading it. Every confusion I've had is because I was reading something you wrote like a description instead of code. Completely my fault – John Smith Aug 05 '16 at 19:36
  • @JohnSmith: heh, that explains the confusion :P I left out the line number mumbo-jumbo that you used in your question, instead preferring to use labels. That code should literally assemble in NASM (with a small tweak to the label declaration), as well as for your toy machine (with something like NASM's macro syntax, but I'd need a different separator for multi-instruction macros since `;` is a comment character in NASM). – Peter Cordes Aug 05 '16 at 19:44
  • I'm going to come back at this a little later. It's still eluding me somewhat. I can see you're illustrating the recursion concept in a general way, which is exactly what I need – John Smith Aug 05 '16 at 20:27
  • @JohnSmith: Are you familiar with recursive functions in asm for normal ISAs like x86? The difference between what I did and what a compiler targeting x86 would do is that gcc would prefer to save/restore a call-preserved register across the whole function, and use that to hold locals across a function call. You can see exactly how gcc does this (with the same calling convention I used, of arg in edi, ret val in eax, in [this gcc output](https://godbolt.org/g/4cY9OH). What I did was save the local on the stack instead of introducing another "register", but that's not what compilers do. – Peter Cordes Aug 05 '16 at 20:42
  • When you say to set eax, edi and ret if n <=1 do you mean to do the set AND the return if edi <=1 or to always do the set, and only return if edi <= 1. I'm confused by the language of having the "if" clause at the end of the sentence – John Smith Aug 06 '16 at 20:54
  • @JohnSmith: It's English pseudocode. It implements the C `if(n<=1) return n`, so yes, it means do both those things if the condition is true. BTW, perl allows actual code like that, with a very-low-precedence `if` operator that you can put at the end of things. But note that unconditionally setting `eax` at that point would have no effect, because `eax` is not an input to the function. It would be a bug if the function depended on the incoming value of `eax`, just like in the SysV calling convention for x86-64. – Peter Cordes Aug 06 '16 at 21:09
  • Is my language capable of doing an "if x < y" logic? I am thinking my "jump if equal to zero" isn't capable of doing it. But my ability to construct higher level constructs is very new and weak – John Smith Aug 06 '16 at 21:33
  • @JohnSmith: It's capable of checking for 0 and then checking for 1. I think I pointed out in a comment on the question that range comparisons in general could be implemented with a loop over every possible value. Keep in mind that this part is totally irrelevant to the recursive part. If you like, simplify it to something that's no longer Fibonacci, but still recursive. This is why I didn't go into any detail about this in the answer, because you're only asking about implementing recursion in this question, not anything else. – Peter Cordes Aug 06 '16 at 21:35
  • So to check <= 1, I would check if zero, then subtract 1 and check that one. if either of those triggered it was 0 or 1. Is that right? – John Smith Aug 06 '16 at 21:38
  • @JohnSmith: yes. We can and should decide that Fib(n) treats its input as an unsigned integer. – Peter Cordes Aug 06 '16 at 21:38
  • I'm trying to repick my jump selections to best implement your code so I can trace through it to understand it better. i would like to have a clever jump that covers all the cases without any ugliness required (like jumpz requires to implement jump-on-a--less-than-b and the latter requires to implement jmpz) Would jump-if-a-less-or-equal-b accomplish this? – John Smith Aug 06 '16 at 21:44
  • because to implement jumpz it would only need to do a check then add 1 and do a check that fails – John Smith Aug 06 '16 at 21:45
  • @JohnSmith: IIRC, minimal instruction sets that are as stripped down as theoretically possible usually include a jump-if-less, or a jump-if-less-or-equal and one other instruction (add or sub I think). (But that's with a register machine with indirect addressing. You *only* have a stack, and no indirection (pointers) to allow use of arrays or variable-size anything) But yes, I think your idea for implementing `jumpz` in terms of `jumple` looks usable. – Peter Cordes Aug 06 '16 at 21:50
  • Oh I see. jump if less than implements jumpz easily too (just a < 0. then a++, then a < 0 – John Smith Aug 06 '16 at 21:53
  • So in terms of how difficult the jumps are to implement other jumps, < and <= are roughly the same? – John Smith Aug 06 '16 at 21:53
  • @JohnSmith: Yes, in hardware you'd just need an adder (set up as a subtractor) for either one. BTW, your instruction set appears pretty crazy to implement in hardware. You could write asm for it that uses more than 2^32 different named variables, so if you want each instruction to use machine code with 32bit absolute addresses, you need to either impose a limit on named variables or use variable-length machine code to handle the unbounded memory-addressing potential. This design doesn't look like it would lend itself to an actual hardware implementation very well. – Peter Cordes Aug 06 '16 at 21:57
  • @JohnSmith: The lack of indirection / pointers is a huge limit on what you can do. If you need to deal with two variable-sized things, I don't think you can even do it. There's only one stack, so unless you always keep your two things interleaved in a constant pattern, there's no way to temporarily pop something, look at something higher up, then put it back. I mean, you could maybe fully unroll a loop that popped into different named variables... Or maybe use function pointers as data? Since you do have indirection there. I haven't thought about that for programmability. – Peter Cordes Aug 06 '16 at 22:01
  • Yeah I'm not surprised it's bad. But it's really just a quick thing because it seemed like making something like it and writing an interpreter in C# would be a fun way to learn some of C#'s more advanced language features. Then I got curious if I could handle things like functions and then I wanted to handle recursion (not for recursion but for having functions arbitrarily call each other with local variables and arguments preserved) – John Smith Aug 06 '16 at 22:02
  • I'm going to make one next with only two instructions: "increment a register and go to a line" and "if register x = 0, jump to line y, else decrement x, and jump to line z – John Smith Aug 06 '16 at 22:08
  • as for multiple variable-sized things, I was planning on learning a little more how arrays are handled at the asm level and trying to do something like that. and I wanted to add pointers too. very annoying not to have those – John Smith Aug 06 '16 at 22:10
  • @JohnSmith: Yeah, it's an interesting exercise to implement as an interpreted language. You asked about the complexity of implementing `<` vs. `<=`, so I had to point out that it's pretty disconnected from what anyone might design for a hardware implementation. – Peter Cordes Aug 06 '16 at 22:11
  • That `inc-and-goto` and if/else jump sounds like a nightmare to program by hand. BTW, you don't need a 2-way jump. Just have a conditional jump that either jumps or falls through like a normal instruction set would use. If you do need a multi-way branch, you can just use more branch instructions. My guess is that it the only way to implement a lot of things would be a nightmare of spaghetti code. If you don't already know a normal asm like x86, ARM, or MIPS, you should probably spend some time playing with one of those. (Although I guess you want a C# exercise too?) – Peter Cordes Aug 06 '16 at 22:14
  • I definitely need to study a real ASM. The two instruction thing was just for the fun of a minimum turing -complete language. are you sure the two way jump isn't necessary for that? – John Smith Aug 06 '16 at 22:19
  • Btw, do you happen to know how Margaret's pastebin'd code executes the mul command. It seems like that code is unreachable. I see an unconditional jump before it with no labels in between – John Smith Aug 06 '16 at 22:32
  • @JohnSmith: not sure. Maybe only if you have multiple registers and a non-destructive RISC-style `sub` instruction that can put the result in a separate register. I'm also not sure about the `inc` instead of `sub`. – Peter Cordes Aug 06 '16 at 22:32
  • @JohnSmith: Margaret's code implements `call` by pushing a modified copy of IP. Note the `add link, 4`, exactly like I mentioned in my answer as an alternative to pushing a label. Since the desired return address is 4 instructions after the `set link, IP`, 4 is the right amount to add to return to right after the `jump`. – Peter Cordes Aug 06 '16 at 22:35
  • Ahh yes. It's just like you said – John Smith Aug 06 '16 at 22:45
  • I transcribed Margaret's code to my interpreter and it goes into an infinite loop. I think it was close to 1:1 from hers but did I miss something possibly? – John Smith Aug 06 '16 at 23:19
  • @JohnSmith: IDK, I didn't read it carefully. I guess it's harder to debug if you're not sure your interpreter works :P Have fun. – Peter Cordes Aug 06 '16 at 23:20
  • Pretty sure. But I was planning on putting some debugging features in to be sure – John Smith Aug 07 '16 at 00:31
  • @JohnSmith: BTW, you can move your check-mark to a different answer if you change your mind about which answer is the best (or a new one appears late; I think you'd already accepted Jose's answer by the time I posted this.) I only point this out in case you didn't know it was ok to do this. I don't mind if you still think the other answer is more worthy of the checkmark. (Also, thanks for fixing your edit to the question. We can delete those last two comments now to unclutter it slightly. We probably should have taken this comment thread to chat ages ago :P) – Peter Cordes Aug 07 '16 at 07:35
0

Margaret's pastebin modified slightly to run in my interpreter for this language: (infinite loop problem, probably due to a transcription error on my part)

set n 3
push n
set initialCallAddress IP
add initialCallAddress 4
push initialCallAddress
jump fact
set finalValue 0
pop finalValue
print finalValue
jump 100
:fact
set rip 0
pop rip
pop n
push rip
set temp n
add n -1
jumpz end n
push n
set link IP
add link 4
push link
jump fact
pop n
mul temp n
:end
pop rip
push temp
jump rip

Successful transcription of Peter's Fibonacci calculator:

        String[] x = new String[] {
            //n is our input, which term of the sequence we want to calculate
                "set n 5",
            //temp variable for use throughout the program
                "set temp 0",
            //call fib
                "set temp IP",
                "add temp 4",
                "push temp",
                "jump fib",
            //program is finished, prints return value and jumps to end
                "print returnValue",
                "jump end",
            //the fib function, which gets called recursively
                ":fib",
            //if this is the base case, then we assert that f(0) = f(1) = 1 and return from the call
                "jumple base n 1",
                "jump notBase",
                ":base",
                "set returnValue n",
                "pop temp",
                "jump temp",
                ":notBase",
            //we want to calculate f(n-1) and f(n-2)
            //this is where we calculate f(n-1)
                "add n -1",
                "push n",
                "set temp IP",
                "add temp 4",
                "push temp",
                "jump fib",
            //return from the call that calculated f(n-1)
                "pop n",
                "push returnValue",
            //now we calculate f(n-2)
                "add n -1",
                "set temp IP",
                "add temp 4",
                "push temp",
                "jump fib",
            //return from call that calculated f(n-2)
                "pop n",
                "add returnValue n",
            //this is where the fib function ultimately ends and returns to caller
                "pop temp",
                "jump temp",
            //end label
                ":end"
        };
John Smith
  • 95
  • 5