Pepsi and Coke (No. 6)

I kind of made it sound like "Pepsi and Coke (No. 5)" was the last one, but that is not necessarily the case^^; I got a simple assignment from Ian.

The assignment (specifically speaking, I said "I want it" first, and then he said "you can make one") is to write the "apply" function. It takes a function and a list (Scheme allows to give more than one list, but it is not essential for now) and call the function with the elements in the list as arguments.

Actually, before I started this assignment, Ian did mention that there are a few VPU instructions missing to implement "apply". Nonetheless, I tried.

I modified compile.st, a source file of Jolt written in Pepsi. For the first-order approximation, I wrote a version that takes spliced arguments (i.e., it is the form of "(apply func arg0 arg1 ...)"). For this one, I've modified the SyntaxTable variable so that it has a line:

	at: #apply	  put: #xApply:;

and wrote a method:

Compiler xApply: aForm
[
   "(apply func args ...)"
    (aForm copyFrom: 1) reverseDo: [:arg | arg compile: self].
    vpu iCalli: aForm size - 2.
   ^ nil
]

As you see, this is a slightly modified version of Compiler>>form:, but it just do one less thing to skip the first "apply" keyword. It worked as expected. This shows that you can modify compile.st. However, what I would like to do is to enable a form:

   (apply func (arg0 arg1 ...))

Here is a vertion with limited functionality:

Compiler xApply: aForm
[
   "(apply func (arg0 arg1 ...)"
    | args func |
    args := (self compile: (aForm at: 2)) value.
    args reverseDo: [:arg | vpu ldPtr: arg].
    (aForm at: 1) compile: self.
    vpu iCalli: args size.
   ^ nil
]

This one only works if it is used at the top-level. You can do something like:

.(define f (lambda (x y z) (+ x y z)))
.(apply f '(5 1 3))

but not much more. The #compile: method for Compiler is meant to be used only for a top-level expression. #compile: has some special vpu messages like #iEnter, #ret, and compile:debug:. In the middle of the message, it has a line that reads: "expr compile: self". From this line, the other methods of the Compiler, such as xLet:, xForm:, and etc. recursively call each other to generate a vpu instruction sequence. This version trys to separate the compilation in two stages by colling it explicitly, but it is incorrect for non-top-level cases. Note that "args size" in this code is called at the code generation time, but it doesn't necessarily reflect the runtime value. To see what it means, you can try something like (in conjunction with the declaration of "apply1"):

Compiler xApply1: aForm
[
    | args func |
    args := Association new.
    (aForm at: 1) compile: self.        "push the first argument to the stack"
    vpu ldPtr: args _valuePointer.      "push the address of 'value' to the stack"
    vpu wrw.               		"store the first argument into 'value'"
    vpu drop.              		"remove the first argument and,"
    vpu ldPtr: args.       		"push args to return it"
    StdOut print: 'compiling';
           print: args; cr.    		"print out the args at code-generation time"
    ^ nil
]

With this definition, when you evaluate "(apply1 '10)" what you see is something like;

.(apply1 '10)
'compiling'(nil -> nil)
(nil -> 10) => 9584804

At the code generation time, the sequence of messages to vpu describes "what it would do at runtime". However, because this "(apply1 '10) is at the top-lvel, the code is immediately executed. At run time, the native code generated via vpu fills the 'value' inst var of the Association and returns it. (Obviously, this code assumes that the objects don't move after it is created). When you evaluate:

.(define f (lambda (x) (apply1 x)))
'compiling'(nil -> nil)
 => 10211728
.[(f '5) print]
(nil -> 5) => 9584804
.[(f '3) print]
(nil -> 3) => 9584804

you see that the code-generation is done just once, and the result from different invocation of "apply1" returns the same object (at the address 9584804 in this particular session). However, different invocations with different arguments fill the 'value' inst var differently. (I hope it somewhat makes clear the distinction of code-generation time and run time.)

What the definition of "apply" really should like like is:

(syntax apply
   (lambda (node comp)
     `(let ((_f ,[node second])
	    (args ,[node third])
	    (size [args size]))
	(apply1 _f args size))))

and, in compile.st:

Compiler xApply1: aForm
[
   "(apply1 func args size)"
    | args func |
    (aForm at: 2) compile: self.   "arg ..."
    vpu stTmp: 1.                  "arg to tmp1"
    (aForm at: 3) compile: self.   "size arg ..."
    vpu stTmp: 3.                  "size to tmp3"
    vpu ldInt: 1                   "1 size arg ..."
    vpu perform: #sub.             "(size-1) arg ..."
    vpu stTmp: 2.                  "(size-1) to tmp2"
    ldInt: 4.                      "4 (size-1) arg ..."
    vpu perform: #mul.             "((size-1)*4) arg ..."
    vpu perform: #add.             "(args + (size-1)*4) ..."
    vpu stTmp: 1.                  "(args + (size-1)*4) to tmp1".
    vpu drop.                      "..."
    vpu ldTmp: 2.                  "(size-i) ..."
    define: 0.
    vpu ldInt: 0.                  "0 (size-i) ..."
    vpu perform: #le.              "(<= size 0) ..."
    vpu bt: 1.                     "..."
    vpu ldTmp: 1.                  "&args[i] ..." 
    vpu rdw.                       "args[(size - i)*4] ..."
    vpu ldTmp: 2.                  "(size - i) args[(size - i)*4] ..."
    vpu ldInt: 1                   "1 (size - i) args[(size - i)*4] ..."
    vpu perform: #sub.             "(size-i) arg ..."
    vpu stTmp: 2.                  "(size-i) to tmp2"
    br: 0.
    vpu define: 1.
    (aForm at: 1) compile: self.
    vpu iCalli: args size.

   ^ nil
]
(There may be a lot of bugs.  This is more like a rough sketch.)  The problem is that the last line of xApply1: has "args size", and this cannot be determined until runtime.  In other words, vpu currently only supports function calls with argument counts known at code generation time.  In short, this version doesn't work, and until we have a different "call" instruction, it wouldn't work.