ICR - Impromptu Compiler Runtime.


The ICR is the most recent addition to the Impromptu family and completely replaces my first aborted attempt at an LLVM compiler for Impromptu (the first impromptu compiler was introduced in 2.0 through the now defunct sys:compile call).  The new compiler is a far more robust and serious attempt at providing low level, but completely dynamic programming support within the Impromptu ecosystem.  It is designed for situations which require low-level systems style programming.  Two primary examples are audio signal processing and opengl programming - both of which require the production of highly efficient code.  While attempting to achieve runtime efficiency the ICR also tries to maintain Impromptu's love of all things dynamic and is designed to coexist with the rest of Impromptu's infrastructure (Scheme, Objective-C, AudioUnits etc..).


It is important to state from the outset that the new Impromptu compiler is NOT a scheme compiler.  It is really its own language.  This language looks like scheme (sexpr's and alike) but is in many ways semantically closer to C (a nice functional version of C :-).  This is because the ICR is designed for purposes that are not suitable for the standard scheme interpreted environment.  Low level tasks that require efficient processing - such as audio signal processing, graphics programming, or general numerical programming.


Unlike Scheme the Impromptu compiler is statically typed.  You may not always see the static typing but it is always there in the background.  The reason that you won't always see the typing is that the compiler includes a type-inferencer that attempts to guess at your types based on static analysis of your code.  Sometimes this works great, sometimes it doesn't.  When things fail, as they surely will, you are able to explicitly specify types to the compiler.


Like all compilers the Impromptu compiler is a translator.  The impromptu compiler takes input in the form of LISP/Scheme s-expressions and translates them into a form of LLVM's intermediate representation (IR) that provides the semantics of our *new* language.  LLVM is a project that supports building compilers.  You can think of LLVM as a kind of CoreAudio, or CoreGraphics, but for compiler builders :)  LLVM's IR is quite similar to assembly language - it is low level, and general.  Once we have compiled into LLVM IR we can leverage LLVM to optimise and compile into x86 machine code.  Finally we load this machine code into our runtime environment - The Impromptu Compiler Runtime (ICR).  The compiler and the compiler-runtime provide a runtime feedback loop with both providing information to each other.


It is important to always remember that this runtime is there, and to note it's distinction from the Scheme runtime.  Impromptu effectively supports three separate language runtimes, Scheme, Objective-C and now the Impromptu compiler runtime (ICR).  There are bridges between these runtimes but they have distinct semantics and it is important to recognise their separation.


As with everything in the Impromptu world, your access to the ICR is through the Scheme interpreter.  Indeed the Impromptu compiler is written in Impromptu Scheme.  You call the compiler through the definec macro.  definec does a lot of things but the main ones you need to be aware of are:


1st: it tries to infer types

2nd: it tries to compile your code (using the type info) into LLVM IR

3rd: it calls LLVM to compile the IR into x86

4th: it generates a stub wrapper function which Scheme can understand


definec may fail at any of the above stages - hopefully with some useful error message!


The 4th stage is important.  The stub wrapper function provides a way for Scheme to talk to ICR.  Scheme does not understand ICR types and ICR does not understand Scheme types, so we need to some way to communicate between the two.  This "stub" function provides that.  It is given the same name as the ICR function (the name you give to definec) and is bound in the Scheme process which you called definec in.  What is important is that you remember that it is NOT the ICR function but a wrapper function with the same name - in other words a Scheme callable function which then calls the real ICR function.


definec is a macro that is semantically very similar to define.  It should feel quite natural for you to use.  Here is a simple function that returns a random value:


(definec my-random

   (lambda ()

      (random)))


You should see the following line printed in your logview

Successfully compiled my-random >>> [double]*


This line tells you that you have compiled my-random - of type [double]* - which is a pointer to a closure (a function and it's environment) that returns a value of type double.


So "Successfully compiled my-random >>> [double]*" tells us that we have successfully compiled a closure which takes no arguments and returns a double, and that we have bound that closure to the name my-random in the ICR as well as producing a wrapper function of the same name and binding it in the current Scheme process.


We can test the wrapper function by simply calling it.


(my-random)


Calling my-random returns a real number.  The wrapper function takes care of translating the double value from the ICR into the Scheme real value which is returned to us.  To also demonstrate that there is a closure of the same name bound in the ICR let us now compile another function which calls my-random.


(definec my-random-2

   (lambda ()

      (my-random)))


Again we have a wrapper returned to us which we can call.  The important thing to note about my-random-2 though is that it calls the ICR version of my-random - not the Scheme wrapper version.  This is a good thing, as we don't want to keep wrapping and unwrapping all the time!  Also we want to avoid having any dependency on the Scheme environment from inside the ICR - we want the ICR to be fast!  Just remember that these two worlds are isolated and only connect through bridges - such as the wrapper functions.  Try the following code that first defines the closure my-gv in Scheme and then tries to compile a closure that uses my-gv.


;; this is a scheme define

(define my-gv

   (lambda ()

      (random)))


;; this is an ICR definec

(definec global-var

   (lambda ()

      (my-gv)))

      

The compiler complains that it doesn't know what my-gv is.  Fair enough - different worlds!  Back to my-random...


The reason that the compiler can tell us that my-random returns a double is that it knows that the (random) call returns a double.  As (random) is the last call in my-random it is obvious that my-random must also return a double.  Let's try making a sqr function:


(definec sqr

   (lambda (x)

      (* x x)))


":ERROR: Compiler Error: could not resolve types for symbols ("sqr" "x") try forcing the type of one or more of these symbols."


Woops could not resolve types for symbol sqr and x.  In other words - what type is x - and until the compiler knows the type of x it can't determine what type sqr returns.  It would seem reasonable that x is a number - given the multiplication - but is it a real or an integer - indeed is it a 32bit float or a 64bit float or an 8bit integer etc..


For times when the type inferencer cannot work out the types for us we can specify types explicitly.  We don't necessarily need to explicitly define all the types that the type inferencer raises because solving one will often solve many.  For example it seems reasonable that if we explicitly type x then the type inferencer will be able to determine sqr.


You explicitly type a symbol using the syntax <symname>:<type>.  For example x:i64 is a 64bit integer.  f:[i32,float]* is a closure which takes a float and returns an i32.


(definec sqr

   (lambda (x:i64)

      (* x x)))


Here we define x to be a 64bit integer "i64".  This also gives a function type for sqr of [i64,i64]*.  A function that takes one 64bit integer argument and returns a 64bit integer.


Let's make a function that takes two arguments:


(definec test-1

   (lambda (x y)

      (* x y)))


As we expect the type inferencer is slacking off and can't help us.  So let's help out and define x as a float.


(definec test-1

   (lambda (x y:float)

      (* x y)))


The type inferencer uses the explicit x type to also type y and to type test-1 which is [float,float,float]* - a closure that takes two float arguments and returns a float (remember the return type comes first).  But what if we needed a function that multiplied a float and a 64bit integer.  ICR has a number of conversion functions for integer and floating point types.  These functions take the form of (i64tof) - for 64bit integer to float, or (ftod) for float to double, or (dtoi8) double to 8bit integer etc..  So if we wrap y in i64tof we should hopefully get the result we want.


(definec test-1

   (lambda (x:float y)

      (* x (i64tof y))))


:ERROR: Compiler Error: sorry the compiler does not currently allow you to redefine or overload the type signature of existing functions. in this case "test-1" to: "[float,float,i64]*" from: "[float,float,float]*"


Hmmm, this isn't quite what we wanted but if you look closely at the error you'll see that it was heading in the right direction.  It does seem that the compiler was at least trying to creating a closure with y as an i64.  At the moment there is a limitation imposed by ICR that says that you can't recompile and rebind an existing closure to a different type signature.  You are free to recompile and rebind an existing closure as often as you like as long as the argument and return types of the closure remain the same.  This current limitation is to protect against the chance that you have used the current "old" definition of the closure in other compiled code.  As long as the type signature remains the same this is fine - but if you change the signature you will break other compiled code that relies on this type signature.


Unfortunately there is currently no function overloading (although this is something I would like to add soon).  So in the interim I'm afraid you'll just have to name your i64 version something else.  Let's go for test-2.


(definec test-2

   (lambda (x:float y)

      (* x (i64tof y))))


Excellent that all works.  Just to prove a point let's change the body of test-2 slightly and recompile.


(definec test-2

   (lambda (x:float y)

      (* x 5 (i64tof y))))


This all works fine because the type signature of test-2 has not been changed.


OK so let's try something out...


;; a sqr function

(definec test-3 

   (lambda (x:float)

      (* x x)))


;; call test-3 with 5.0

(definec test-4

   (lambda ()

      (test-3 5.0)))


;; should give us 25.0

(test-4)

;; and again 25.0

(test-4)


;; recompile sqr to cube

(definec test-3

   (lambda (x:float)

      (* x x x)))


;; ???

(test-4) ;; wow 125.0 and we didn't recompile test-4


This is pretty neat really remembering that this is all happening dynamically - test-4 automatically switches over to using the new machine code compiled from our redefined test-3.  We could switch back to the sqr version of course by recompiling that version.  Just remember that any recompiles are fine so long as you don't change the closures signature - this restriction allows other ICR machine code to rebind to newly compiled machine code on-the-fly.



Some Detail

OK, so now we have the basics covered let's get into some of the gritty details.  


Here are the ICR's primary types:


i1 -> boolean

i8 -> 8bit byte

i32 -> 32bit integer

i64 -> 64bit integer

float -> 32 bit floating point

double -> 64 bit floating point

[type,type ...] -> is a closure including a return type followed by 'n' argument types

<type,type ...> -> is a tuple which contains 'n' number of types


All of the above can pointers - indeed tuples and closures are always pointers.  A pointer type uses '*' as in C.  So for example, an i8 pointer is i8* a pointer to a closure that returns a double and has a 32 bit integer argument would be [double,i32]* etc..  Strings are represented as i8*.  void pointers are also represented as i8*.


It is possible to allocate memory using (make-array) which allows you to allocate 'n' number of 'type'.  (make-array 8 double) will return a double* which points to the first double in an array of 8 doubles.  You are responsible for calling free on any memory that you allocate.  (make-array) returns a pointer to the type you defined.  So (make-array 8 double) returns double*.  (make-array 8 double*) returns double**.  (make-array 8 [double]*) returns [double]** etc..


You can "cast" numbers using the ftoi64, dtof, i32toi64 etc.. as discussed above

You can also "cast" pointers - where the first argument is a pointer value and the second argument is a pointer type to cast to - using bitcast: (bitcast my-ptr i8*) -> casts my-ptr to i8*.  Don't forget complex types - this is perfectly valid: (bitcast my-ptr (closure* double double)).  However, as per C this is NOT type safe - you will crash Impromptu if you don't know what you're doing.  If in doubt don't use bitcast!!


Variables are introduced and scoped using closures and lets as per Scheme.  Let in the ICR is similar to let* in Scheme and can be used for recursive closure definition - similar to letrec.  Closures in the ICR are first-class, you can pass them as arguments, store them in tuples or arrays or return them as per Scheme.


Some mathematical functions are overridden depending on type - here is a complete list of those that are:

* / - + override depending on type - for both reals and integers.

< > = <> also override depending on type - for both reals and integers.  These always return a boolean result (i1).

modulo is overridden for real and integer types.

dotimes overrides it's incrementor depending on type (real or integer)


For all other operations are of singular type.


This is a complete list of ICR's built-in functions:

* / - + < > = <> ;; integer or real -> returns i1 (bool)

(modulo real real) or (modulo int int) ;; as per impromptu

(dotimes (i integer|real) ...) ; as per impromptu

(printf "format-string" ...) ; as per C.

(random) -> returns double

(random i64) -> returns i64

(random i64 i64) -> returns i64

if statements as per scheme

cond statements as per scheme

and or as per scheme

#t and #f as per scheme

null for null pointer

i8* malloc(i64) and free (free overrides on any pointer type - be careful!)

(array-ref ptr index) -> where ptr is any pointer, index is any integer and the return type is a de-reference of the pointer. (i.e. a pointer double* returns a double) - array-ref has an alias aref

(array-set! ptr index value) -> where ptr is any pointer, index is any integer, value is a de-reference of the pointer. (i.e. you set a value of type double in a ptr of type double*), returns the value. - array-set! has an alias aset!

(tuple-ref tuple index) -> where tuple is a tuple pointer (tuple*) and index is an integer.  The return type is the type of the value at tuple index.  tuple-ref has an alias tref.

(tuple-set! tuple index value) -> where tuple is a tuple pointer (tuple*) and index is an integer, the value must be the same type as tuple index.  Returns value.  tuple-set! has an alias tset!

(make-tuple type1 type2 type3 …) where type is any valid type.

lambda -> as per scheme

let -> like scheme let*

set! -> as per scheme


Additionally the ICR includes most of the standard C library.  Particularly functions from the math.h, stdio.h and string.h libraries.  Their operation is semantically identical to C99 with the following caveats. Types void* and char* from C99 are both represented in ICR as i8*.  Structs from C99 are tuples in ICR.  No variable argument functions are supported (the only exception being printf as above).


After reading all this you should be getting the sense that the ICR is like a dynamic "C" with first-class closures and type-inference.


An Example Tutorial


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; some simple stuff


;; multiple a * 5

;; note that type infercing works out the type 

;; of "a" and then using the inferred type

;; also works out the type of my-test-1

;; (i.e. argument type and return type)

;; 

;; integer literals default to 64 bit integers

(definec my-test-1 

   (lambda (a)

      (* a 5)))


;; notice that the log view displays the type

;; of the closure we just compiled

;; [i64,i64]*

;; The square brackets define a closure type

;; The first type within the square braces is

;; the return type of the function (i64 for 64bit integer)

;; Any remaining types are function arguments 

;; in this case another i64 (for 64bit integer)

;; 

;; All closures are pointers.  Pointer types are

;; represented (as in "C") with a "*" which trails

;; the base type.

;; So an i64 pointer type would be "i64*"

;; A double pointer type would be "double*"

;; So a closure pointer type is "[...]*"


;; float literals default to doubles

(definec my-test-1f

   (lambda (a)

      (* a 5.0)))


;; Again note the closures type in the logview

;; [double,double]*



;; we can call these new closures like so

(print (my-test-1 6)) ;; 30

(print (my-test-1f 6.0)) ;; 30.0



;; you are free to recompile an existing compile

;; closures body to do something different whenever you like

;; so we can change my-test-1 to

(definec my-test-1

   (lambda (a)

      (/ a 5)))


(print (my-test-1 30)) ; 30 / 5 = 6


;; note that the closures signature is still the same

;; as it was before.  This is important because we are

;; NOT allowed to change an existing compiled closures

;; type signature.

;; 

;; So we CANNOT do this

(definec my-test-1

   (lambda (a)

      (/ a 5.0)))


;; Just remember that you are not currently allowed to redefine an 

;; existing function to a new definition that requres a different type signature.  

;; This is to protect against the situation where you have allready compiled

;; code which requires the current signature.



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Because we are working with closures

;; we can close over free variables

;; in this example we close over power

;; to maintain state between calls

;;

;; increment power each call

(definec my-test-2

   (let ((power 0))

      (lambda (x)

         (set! power (+ power 1)) ;; set! for closure mutation as per scheme

         (* x power))))


;; each modifies state

(print (my-test-2 2)) ;; should = 2

(print (my-test-2 2)) ;; should = 4

(print (my-test-2 2)) ;; etc


               

;; Closures can of course return closures.

;; notice the type signature of this function

;; as printed in the logview "[[i64,i64]*]*"

;; being a closure that returns a closure

(definec my-test-3

   (lambda ()

      (lambda (x)

         (* x 3))))



;; let's try to make a generic incrementor

;;

;; here we run into trouble

;; because the type inferencer cannot infer a valid type 

;; for i or inc and therefore also cannot infer

;; a type for my-inc-maker!

;;

;; THIS WILL CAUSE AN ERROR!

(definec my-inc-maker

   (lambda (i)

      (lambda (inc)

         (set! i (+ i inc))

         i)))


;; This makes sense should "+" operate

;; on doubles or integers - who knows?

;; So the type inferencer complains justifiably complains

;;

;; What can we do about this ... 

;; we need to help the compiler out by proving some

;; explicit type goal posts

;;

;; We can do that by "typing" a variable.

;; Explicitly typing a variable means tagging

;; the symbol with a type separated by ":"

;;

;; Here are some examples

;; x:i64        = x is a 64 bit integer

;; y:double     = y is a double

;; z:i32*       = z is a pointer to a 32 bit integer

;; w:[i64,i64]* = w is a closure which takes an i64 and returns an i64

;;

;; Make sure there are no spaces in the expression

;;

;; Now we can explicitly type i

(definec my-inc-maker

   (lambda (i:i64)

      (lambda (inc)

         (set! i (+ i inc))

         i)))


;; this solves our problem as the compiler

;; can now use i's type to infer inc and

;; therefore my-inc-maker.



;; now we have a different problem.

;; if we call my-inc-maker we expect to be 

;; returned a closure.  But Scheme does not

;; know anything about ICR closure types and therefore

;; has no way of using the returned data.  Instead

;; it places the returned pointer (remember a closure is a pointer)

;; into a generic Scheme cptr type.

;;

;; We are free to then pass that cptr back into another

;; compiled function as an argument.  

;; 

;; So let's build a function that excepts a closure returned from 

;; my-inc-maker as an argument, as well as a suitable operand, and 

;; apply the closure.


;; f is our incoming closure

;; and x is our operand

;; THIS WILL CAUSE AN ERROR

(definec my-inc-maker-wrappert

   (lambda (f x) ; f and x are args

      (f x)))


;; oops can't resolve the type of "f"

;; fair enough really.

;; even if we give a type for "x"

;; we still can't tell what "f"'s

;; return type should be?

;; This also causes an error!

(definec my-inc-maker-wrappert

   (lambda (f x:i64) ; f and x are args

      (f x)))


;; so we need to type f properly

(definec my-inc-maker-wrapper

   (lambda (f:[i64,i64]* x)      

      (f x)))


;; ok so now we can call my-inc-maker

;; which will return a closure

;; which scheme stores as a generic cptr

(define myf (my-inc-maker 0))


;; and we can call my-in-maker-wrapper

;; to appy myf

(print (my-inc-maker-wrapper myf 1)) ; 1

(print (my-inc-maker-wrapper myf 1)) ; 2

(print (my-inc-maker-wrapper myf 1)) ; 3 etc..


;; of course the wrapper is only required if you 

;; need interaction with the scheme world.

;; otherwise you just call my-inc-maker directly


;; this avoids the wrapper completely

(definec my-inc-test

   (let ((f (my-inc-maker 0)))

      (lambda ()

         (f 1))))


(print (my-inc-test)) ; 1

(print (my-inc-test)) ; 2

(print (my-inc-test)) ; 3


;; hopefully you're getting the idea.

;; note that once we've compiled something

;; we can then use it any of our new

;; function definitions.



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; a simple tuple example

;; 

;; tuple types are represented as <type,type,type>*

;;


;; make and return a simple tuple

(definec my-test-6

   (lambda ()

      (make-tuple i64 double i32)))


;; logview shows [<i64,double,i32>*]*

;; i.e. a closure that takes no arguments

;; and returns the tuple <i64,double,i32>*

      


;; here's another tuple example

;; note that my-test-7's return type is inferred

;; by the tuple-reference index 

;; (i.e. i64 being tuple index 0)

(definec my-test-7 

   (lambda ()

      (let ((a (make-tuple i64 float)) ; type <i64,float>

            (b 37)

            (c 6.4))

         (tuple-set! a 0 b) ;; set i64 to 64

         (tuple-set! a 1 c) ;; set float to 6.4

         (tuple-ref a 0)))) ;; return first element which is i64


;; should be 64 as we return the 

;; first element of the tuple 

(print (my-test-7)) ; 37



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; some array code with *casting*

;; this function returns void

(definec my-test-8

   (lambda ()

      (let ((v (make-array 5 float)))

         (dotimes (i 5)

            ;; random returns double so "truncate" to float

            ;; which is what v expects

            (array-set! v i (dtof (random))))

         (dotimes (k 5)

            ;; unfortunately printf doesn't like floats

            ;; so back to double for us :(

            (printf "val: %lld::%2f\n" k (ftod (array-ref v k)))))))


(my-test-8)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; some crazy array code with closures

;; try to figure out what this does


(definec my-test-9 

   (lambda (v:i64*)

      (let ((f (lambda (x)

                  (* (array-ref v 2) x))))

         f)))


(definec my-test-10 

   (lambda (v:[i64,i64]**)

      (let ((ff (aref v 0))) ; aref alias for array-ref

         (ff 5))))


(definec my-test-11

   (lambda ()

      (let ((v (make-array 5 [i64,i64]*)) ;; make an array of closures!

            (vv (make-array 5 i64)))

         (array-set! vv 2 3)

         (aset! v 0 (my-test-9 vv)) ;; aset! alias for array-set!

         (my-test-10 v))))


;; try to guess the answer before you call this!!

(print (my-test-11))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; some conditionals


(definec my-test-12

   (lambda (x:i64 y)

      (if (> x y)

          x

          y)))


(print (my-test-12 12 13))

(print (my-test-12 13 12))


;; returns boolean true

(definec my-test-13

   (lambda (x:i64)

      (cond ((= x 1) (printf "A\n"))

            ((= x 2) (printf "B\n"))

            ((= x 3) (printf "C\n"))

            ((= x 4) (printf "D\n"))

            (else (printf "E\n")))

      #t))

            

(my-test-13 1)

(my-test-13 3)

(my-test-13 100)



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; making a linear envelop generator

;; for signal processing and alike


(definec envelope-segments

   (lambda (points:double* num-of-points:i64)

      (let ((lines (make-array num-of-points [double,double]*)))         

         (dotimes (k num-of-points)

             (let* ((idx (* k 2))

                    (x1 (aref points (+ idx 0)))

                    (y1 (aref points (+ idx 1)))

                    (x2 (aref points (+ idx 2)))

                    (y2 (aref points (+ idx 3)))

                    (m (if (= 0.0 (- x2 x1)) 0.0 (/ (- y2 y1) (- x2 x1))))

                    (c (- y2 (* m x2)))

                    (l (lambda (time) (+ (* m time) c))))

                (aset! lines k l)))

         lines)))


(definec make-envelope

   (lambda (points:double* num-of-points)

      (let ((klines:[double,double]** (envelope-segments points num-of-points))

            (line-length num-of-points))

         (lambda (time)

            (let ((res -1.0))

               (dotimes (k num-of-points)

                  (let ((line (aref klines k))

                        (time-point (aref points (* k 2))))

                     (if (or (= time time-point)

                             (< time-point time))

                         (set! res (line time)))))

               res)))))


;; make a convenience wrapper 

(definec env-wrap

   (let* ((points 3)

          (data (make-array (* points 2) double)))

      (aset! data 0 0.0) ;; point data

      (aset! data 1 0.0)      

      (aset! data 2 2.0)

      (aset! data 3 1.0)      

      (aset! data 4 4.0)

      (aset! data 5 0.0)

      (let ((f (make-envelope data points)))

         (lambda (time:double)

            (f time)))))


(print (env-wrap 0.0)) ;; time 0.0 should give us 0.0

(print (env-wrap 1.0)) ;; time 1.0 should give us 0.5

(print (env-wrap 2.0)) ;; time 2.0 should be 1.0

(print (env-wrap 2.5)) ;; going back down 0.75

(print (env-wrap 4.0)) ;; to zero



TYING IT ALL TOGETHER


OK this is all well and good, but why do I care?  Let's take audio signal processing as our "killer" project and look at how we tie Scheme, Objective-C, the ICR and the AudioUnit framework together to form a powerful dynamic, realtime, audio signal processing environment.


AUCODE SHELLS

The first thing we need to discuss are two AudioUnits which ship with Impromptu 2.5+.  They are ("aumf" "code" "MOSO") and ("aumu" "code" "MOSO").  Both of these audiounits provide shells which you can "fill" with code compiled with the ICR.  The "aumf" works as either an audio effect (if it takes an input) or a generator (without an input).  The "aumu" is an instrument which can be played just like any other AudioUnit instrument.  Both of these AU's can be instantiated as many times as you like (i.e. you can have as many instances of them running as your system can handle), each with it's own ICR kernel function which you set using (au:code:set!).  You are free to place these AU's anywhere that you would usually place an AU giving you the ability to mix and match your custom ICR code with existing free and commercial AudioUnits.


So what "code" do we send to these "code" AU's.  Simply put an ICR closure - the type of the closure depends on whether we are using an "aumf" or an "aumu".  


An "aumf" needs a closure that takes an incoming sample (double), the time of the incoming sample (double),  the channel of the incoming sample (double), and a data object that contains some AU user definable parameter data (double*).  The closure should return a double which is the sample it has generated.  If the "aumf" is operating as a generator the sample argument is still required but will always be a value of 0.0.  The complete closure signature for the closure that you use in the "aumf" is AND MUST BE [double double double double double*]*.  


The simplest ICR closure that we could use as the kernel for an "aumf" would be a noise kernel, so let's make one.  But first we need to load the AU into the AU graph as usual.



(define code1 (au:make-node "aumf" "code" "MOSO"))

(au:connect-node code1 0 *au:output-node* 0)

(au:update-graph)


;; I like to call the function kernel1

;; to match the code1 of the AU I'm

;; going to use this closure with

;;

;; define a kernel function in ICR

(definec kernel1

   (lambda (sample time channels data)

      (random)))


Unfortunately this will not compile because the type-inferencer cannot "guess" what type sample time channels and data are.  Given that we aren't using them for anything this makes sense.  However, the "aumf" needs a particular closure signature so we are obliged to provide these arguments even if we don't use them.  So we'll have to specify them manually:


;; define a kernel function in ICR

;; either explicity define each argument type

(definec kernel1 

   (lambda (sample:double time:double channels:double data:double*)

      (random)))


;; or instead we could define the signature of the closure itself

;; we specify the signature of the function as a consd pair 

;; of (syname . type) following the name we are giving the closure

(definec kernel1 

   (kernel1 . [double,double,double,double,double*]*)

   (lambda (sample time channels data)

      (random)))


Whichever way you do it we need the compiler to return us the closure type [double double double double double*]*.


Successfully compiling the kernel should display the correct closure type.


Successfully compiled kernel1 >>> [double double double double double*]*


OK now that we have compiled the kernel1 we need point the AU to the compiled code.  We do this by telling the AU the NAME of the closure.  Note that we don't want to pass the Scheme wrapper to the ICR closure (which we would do if we did (au:code:set! code1 kernel1)).  Instead we pass the name of the ICR closure as a string to the AU and let the AU fetch the proper ICR closure directly from the ICR.  You don't need to worry about this, it happens on your behalf.


(au:code:set! code1 "kernel1")


This is set and forget.  Once you have assigned the name of an ICR closure to the AU then any recompiles of the kernel closure (not changing the closures signature of course) will automatically update the kernel of the AU dynamically.  Try recompiling this:


;; or instead we could define the signature of the closure itself

(definec kernel1

   (lambda (sample:double time:double channels:double data:double*)

      (* .1 (random))))


You should hear the amplitude of the signal change as soon as the closure is compiled.

This is fun!  Let's recompile again!


;; how about a sine tone in the right channel

;; and noise in the left

(definec kernel1

   (lambda (sample:double time:double channels:double data:double*)

      (if (> channels 0.0)

          (* .5 (sin (* 2.0 3.141592 time (/ 440.0 44100.0))))

          (* .1 (random)))))


etc.. you should be getting the idea!  OK, now, let's talk about state.  One of the problems with the above example, is that if we change the frequency of the oscillator and recompile we hear a click.  You might like to change both left and right to sine tones to more easily here this - remember we can change the guts of kernel1 as much as we like as long as we don't change the closures signature!  We are also going to assign one of the AudioUnit's built in parameters to the frequency.  You'll see that the "aumf" "code" "MOSO" has ten floating point parameters.  These are all between 0.0 and 1.0.  These are the first 10 double values assigned to data and you can access them using array-ref (or the alias aref).


;; osc left and right

(definec kernel1

   (lambda (sample:double time:double channels:double data:double*)

      (let ((freq (* 1000.0 (aref data 0))) ;; frequecy 0.0 - 1000.0

            (tone (* .5 (sin (* 2.0 3.141592 time (/ freq 44100.0))))))

         (if (> channels 0.0)

             tone

             tone))))


Note that when we first compile this we go to silence - this is because AU Parameter One is set to 0.0 by default - hence a frequency of 0.0 for our kernel.  You can change the value of the parameter exactly as you would for any audiounit in impromptu either (a) open the AU's view and physically move the parameter or (b) call (au:set-param).  I suggest opening the AU view for this example.


Now as you slide around Parameter One you'll hear the horrible clicking!  For those without an audio background this is because we are constantly screwing the phase of the oscillator each time we adjust the frequency.  What we need is a better oscillator.  Also seeing as oscillators are so integral to any signal processing let's make something that we can reuse.   Let's do that.  Here's my simple Oscillator builder closure.


;; define an oscillator builder

(definec make-oscil

   (lambda (phase)      

      (lambda (amp freq)

         (let ((inc (* 3.141592 (* 2.0 (/ freq 44100.0)))))

            (set! phase (+ phase inc))

            (* amp (sin phase))))))


Let's take a look at what we just built

Successfully compiled make-oscil >>> (closure* (closure* double double double) double)

A closure that takes a phase (double) as an argument and returns a closure that takes an amplitude (double) and a frequency (double) and returns a value (double).  Importantly we can build as many oscillator closures as we like by calling make-osc repeatedly.  Each one will maintain it's own phase state.


Now the idea is that we can use this make-oscil in kernel1 to maintain phase while sweeping across frequencies.  In order to do this we need to maintain the oscillator returned by calling make-oscil between individual calls to kernel1 (remember that kernel1 get's called for every single sample needed by your audio device).  This is easy because kernel1 is a closure of course :)


;; oscs for left and right

(definec kernel1 

   (let ((my-oscl (make-oscil 0.0)) ;; left oscillator

         (my-oscr (make-oscil 0.0))) ;; right oscillator

      (lambda (sample:double time:double channels data:double*)

         (let ((freq (exp (* 7.0 (+ .4 (aref data 0)))))) ;; AU Parameter One

            (if (> channels 0.0)

                (my-oscl .5 freq)

                (my-oscr .5 freq))))))


So now we have two oscillators (left and right channels) that maintain their phase state, between calls to kernel1.  Using closures to maintain state is going to be a standard trick in all of our signal processing.  It is worth noting that my-oscl and my-oscr act like individual unit generators and the make-oscil closure is like a factory for making oscillators.  We can make as many as we like.  Of course unlike more transitional music 'n' languages we don't need to rely on unit generators being provided for us - we can role our own - in real-time!!


Let's wipe all that and start again.  This time we're going to create a simple "aumu".  The "aumu" "code" "MOSO" AU works just like the "aumf" "code" "MOSO" version but instead of using an ICR closure (kernel) that returns a double (audio sample), it uses an ICR closure that returns a closure that returns a double.  This is because each individual note played by the "aumu" needs it's own closure with it's own independent state.  The only other difference is that where the "aumf" takes a sample (double), the "aumu" takes a frequency (double).  The full type signature of the "aumu" closure looks like this:  [[double double double double double*]*]*.  A closure that returns a closure that returns a double and takes double, double, double, double* as args.


OK here we go


;; This time connect an AUMU into an AUMF

;; i.e. an instrument into an effect

(define code1 (au:make-node "aumu" "code" "MOSO"))

(define code2 (au:make-node "aumf" "code" "MOSO"))

(au:connect-node code1 0 code2 0)

(au:connect-node code2 0 *au:output-node* 0)

(au:update-graph)


;; we'll need our oscillator

(definec make-oscil

   (lambda (phase)      

      (lambda (amp freq)

         (let ((inc (* 3.141592 (* 2.0 (/ freq *samplerate*)))))

            (set! phase (+ phase inc))

            (* amp (sin phase))))))


;; here is a very simple sinewave instrument

(definec sine-inst

   (lambda ()

      (let* ((losc (make-oscil 0.0))

             (rosc (make-oscil 0.0)))

         (lambda (freq:double time:double channel data:double*)

            (if (> channel 0.0)

                (rosc 0.5 freq)

                (losc 0.5 freq))))))


;; set the sine-inst as the kernel of code1

(au:code:set! code1 "sine-inst")


;; now play the instrument just like any other AUMU

(define loop

   (lambda (beat) 

      (dotimes (i 16)

         (play (* i 1/4) code1 (pc:random 70 90 '(0 2 3 5 7 8)) 80 1/8))

      (for-each (lambda (p)

                   (play (* 3/2 (random 4)) code1 (+ p 12) 80 .5)

                   (play code1 p 80 1))

                (pc:make-chord 40 70 4 (random '((0 3 7) (2 5 8) (7 11 2)))))

      (callback (*metro* (+ beat (* 1/2 4))) 'loop (+ beat 4))))


(loop (*metro* 'get-beat 4))

 

;; note that you can change the default attack, release and gain

;; of the "aumu" "code" "MOSO"

(au:open-view code1)

;; or using au:set-param

(au:set-param (now) code1 1 *au:global-scope* 0 .01) ;;attack

(au:set-param (now) code1 2 *au:global-scope* 0 .01) ;;release



At this stage the "aumf" has no kernel and is just passing sample data straight through.  Let's change that and give the "aumf" a delay kernel.


;; First I'm going to make a helper function

;; for a simple delay line

(definec delay-line 

   (lambda (size)

      ;; first make an 'size' length delay line      

      (let ((line (make-array size double)))

         ;; zero out delay line

         (dotimes (i size) (array-set! line i 0.0))

         ;; return delay-line closure

         (lambda (time x:double decay)

            ;; get our time into the delay line

            ;; as a module of time

            (let* ((pos (modulo (dtoi64 time) size))

                   (result (* decay (array-ref line pos))))

               (array-set! line pos (+ x result))

               result)))))


;; then we build a simple aumf kernel that uses delay-line

;; make left and right channels different delay lengths

(definec delay-kernel

   (let ((dell (delay-line 7350))

         (delr (delay-line 11025)))

      (lambda (sample:double time:double channel data:double*)

         (if (> channel 0.0)

             (* 0.7 (delr time sample 0.6))

             (* 0.7 (dell time sample 0.6))))))


;; assign delay-kernel to code2

(au:code:set! code2 "delay-kernel")


Of course as in previous examples, you can recompile these closures at anytime to make changes.  In the case of the "aumf" the change will take effect as soon as the closure compiles.  In the case of the "aumu" the changes will affect the next note that is played.  Try recompiling sine-inst with the code below:


;; let's grunge up the right channel of the sine inst

(definec sine-inst

   (lambda ()

      (let* ((losc (make-oscil 0.0))

             (rosc (make-oscil 0.0)))

         (lambda (freq:double time:double channel data:double*)

            (if (> channel 0.0)

                (* .3 (lgamma (rosc .5 (* 2. freq))))                

                (losc 0.5 freq))))))


As a final example let's look at an example that mixes Scheme, Objective-C and the ICR.  A very simple audio file playback kernel with a randomizing playhead.


(define code (au:make-node "aumu" "code" "MOSO"))

(au:connect-node code 0 *au:output-node* 0)

(au:update-graph)


;; load audio file

(define adat (au:load-audio-data "/tmp/my.mp3"))


;; retrieve raw floating point audio data from adat

(define audiodat (objc:object-at-index adat 3))


;; bind the raw audio data as a global variable

;; of type float* in the IRC

;;

;; this is the only other way to communicate between Scheme and the IRC

;; 

;; You can only pass Scheme cptr objects such as that

;; returned from the call to (objc:call audiodat "bytes")

;; and must assign a valid IRC pointer type (float* in this case)

(bindc audiodat float* (objc:call audiodat "bytes"))


;; also assign the files number of channels and frames as global vars

(define audiodat2 (objc:data:make (* 8 2)))

(objc:data:set-sint64 audiodat2 0 (objc:get-value-for-key (objc:object-at-index adat 0) "frames"))

(objc:data:set-sint64 audiodat2 1 (objc:get-value-for-key (objc:object-at-index adat 0) "channels"))

;; and bind them

(bindc audiodat2 i64* (objc:call audiodat2 "bytes"))


;; sample playback kernel with randomizing playhead

;; you might want to fiddle with phoffset

;; depending on the length of your audiofile

(definec sample-playback

   (lambda ()

      (let* ((phoffset 600000)

             (playhead phoffset))

         (lambda (freq:double time:double channel:double data:double*)

            (let ((frames (array-ref audiodat2 0))

                  (channels (array-ref audiodat2 1)))

               (if (> (random) (array-ref data 0))

                   (set! playhead (+ phoffset (dtoi64 (* (random) 1000000.0)))))

               (if (= channel 0.0) (set! playhead (+ playhead 1)))

               (ftod (array-ref audiodat (+ playhead (* (dtoi64 channel) frames)))))))))


;; set playback kernel

(au:code:set! code "sample-playback")


;; set randomize parameter to 1.0 (not randomized)

(au:set-param (now) code 3 *au:global-scope* 0 1.0)

;; play a note

(play-note (now) code 60 80 (* *second* 30.0))

;; randomize the playback head

(au:set-param (now) code 3 *au:global-scope* 0 0.9999)


This document only starts to scratch the surface of what's possible but will hopefully give you somewhere to start and some idea of what the ICR is and why it might be useful.  The ICR allows you to write, compile and bind "C style" code dynamically, in real-time at runtime.  This allows you to do some cool things, like writing and modifying the executable code of AudioUnits dynamically at runtime.  With the ICR you don't need to "call out to some lower level" for runtime efficiency, the ICR *is* that lower level efficiency.  If there is a particular unit generator that you need, and it doesn't exist yet, you can build it at runtime without bankrupting your CPU!



Memory Management and Zones


Most memory management in the ICR is stack based, meaning that whatever allocations you make are freed automatically when the stack is popped.  There are three exceptions to this stack management in Impromptu - closures, tuples and arrays.  Whenever you call make-tuple, make-array or lambda you are allocating memory on the heap.  In most languages with higher level functions - closures and first class functions - heap management is controlled by a garbage collector.   This is the case for the Impromptu interpreter for example.  However, in an effort to keep the ICR as stream lined as possible I have decided to leave heap management as an explicit concern of YOU and the runtime.


To help make heap management more manageable the ICR provides a concept of multiple heaps - called zones.  Each zone presents an expandable memory container into which you can make any number of memory allocations.  When the memory allocated in the zone is no longer required you can free the zone and reclaim all of the allocations made into it.


Memory zones are often created and destroyed behind the scenes by the ICR.  Examples include when you compile a closure the ICR adds the compiled closure to a new zone.  The zone is automatically freed if you recompile the closure - thus freeing the previous manifestation of the closure.  Another example is the "aumu" "code" "MOSO" which creates and destroys a new zone to hold the closure that is instantiated for each and every note that is performed.


However, there are times when you must take explicit control over creating and freeing zones.  In all of the code presented previously in this document whenever you have called a particular wrapper function from Scheme this has resulted in a memory leak.  The reason is that by default the "default zone" is used to store any wrapper calls.  The default zone is like a global catch all and you are not allowed to explicitly free the default zone.  So anything allocated to the default zone will result in a memory leak.  


The answer to leaky ICR calls is to create a new zone and set it as the storage zone for subsequent wrapper calls.  When you are finished making one or more wrapper calls you can free the zone and reclaim the used memory.  Simple as that ... almost ...


There is one major caveat to keep in mind.  If your wrapper returns memory that you wish to keep around (i.e. a closure, an array or a tuple) then you need to maintain the zone into which it was allocated for as long as you wish to maintain your Scheme reference to the returned data.  Failure to do so will crash Impromptu!


Here's some examples code to help demonstrate some zone usage.  You might like to open the OSX Activity Monitor to watch Impromptu memory use while trying these examples:



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; create a closure that will allocate

;; and return an array

(definec leak1

   (lambda (size)

      (let ((a (make-array size double)))

         (dotimes (i size)

            (aset! a i (i64tod i)))

         a)))


;; run this a few times and watch your memory leak!

(define a (leak1 1000000))


;; to avoid this we need to use zones

;; to capture any allocated memory

;; first define a new zone

(define zone (icr:new-zone))

;; then set the zone into the ICR

(icr:set-zone zone)


;; now check (and remember) what your current memory 

;; use is and then call leak a few more times

(define a (leak1 1000000))


;; reclaim the memory by destorying the zone

(icr:destroy-zone zone)



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Create a closure that returns a closure


(definec leak2

   (lambda (x:i64)

      (lambda (y)

         (* x y))))


;; make a wrapper call for leak2's returned clousre

(definec wrapper

   (lambda (f:[i64,i64]* z)

      (f z)))


;; now lets make a new zone

(define z1 (icr:new-zone))

;; and set the zone

(icr:set-zone z1)


;; call leak2 to return a closure

(define f (leak2 5))

;; now use f with wrapper

(print (wrapper f 3)) ; 15

(print (wrapper f 3)) ; 15

(print (wrapper f 3)) ; 15


;; all is good.

;; let's create f2 in a different zone

(define z2 (icr:new-zone))

(icr:set-zone z2)


(define f2 (leak2 4))

;; now use f2 with wrapper

(print (wrapper f2 3)) ; 12

(print (wrapper f2 3)) ; 12

(print (wrapper f2 3)) ; 12


;; it's all good.

;; now destory z2

(icr:destroy-zone z2)


;; we can still use f

;; because f is in z1

(print (wrapper f 3)) ; 15

(print (wrapper f 3)) ; 15

(print (wrapper f 3)) ; 15


;; HOWEVER!!! if we try to call

;; f2 we will crash impromptu

;; because the memory backing f2 

;; was reclaimed when we destroyed z2

(print (wrapper f2 3)) ; -> crash!!!


;; the observant viewer may have wondered

;; what zone we are capturing in since 

;; destorying z2 seeing as that was the 

;; currently set zone ... the answer is that

;; whenever you destroy a zone the ICR drops

;; back to the default zone - which is never

;; allowed to be destroyed.



As a final note on Zones:


Firstly if you are using the ICR exclusively with audiounits for signal processing then you don't really need to worry about zones as zoning is handled for you by the AudioUnit.  


Secondly if you use malloc or free in your code these allocations happen OUTSIDE of the zone and must be handled by you explicitly - this is the whole point of giving you explicit access to malloc and free.  If you don't understand what I'm talking about don't use them!  


Finally if you are really concerned about working with zones just leave the default zone in place and leak memory.  Often times the amount of memory leaked is small enough to be ignored.   If the leak gets serious enough you can start investigating the ins and outs of zones :)



ICR Libraries


The ICR includes bindings to four significant libraries.  Firstly the standard C libraries (stdio.h string.h & math.h).  Secondly OpenGL. Thirdly CoreFoundation.  And fourthly the vDSP library.  I'm sure I'll get around to talking about these more in the future but in the mean time here are some examples that you should be able to extrapolate from.  Note that these bindings are nearly - but not totally complete.  There are some current ICR restrictions that make some bindings illegal (particularly functions with variable length arguments such as fprintf - note that printf is supported as a *special case*).  



STDIO EXAMPLES


;; write str to path using stdio

(definec file-test

   (lambda (path str)

      (let ((file (fopen path "w")))

         (fputs str file)

         (fclose file))))

      


(file-test "/tmp/mytest.txt" "hello world")



;; simple string example

(definec concat

   (lambda (s1 s2)

      (let ((lgth (strlen s2)))

         (strncat s1 s2 lgth))))


(define str-cptr (concat "hello " "world"))

;; turn i8* into scheme string

(define str (cptr->string str-cptr))

;; print scheme str

(print str)



OPENGL EXAMPLE


;; compile opengl code

(definec gltest

   (lambda (ctx time:double)

      (CGLSetCurrentContext ctx)

      (CGLLockContext ctx)

       (glClear (+ GL_DEPTH_BUFFER_BIT GL_COLOR_BUFFER_BIT))      

      (glLoadIdentity)

      (dotimes (i 100)

         (glRotated (* 0.25 (i64tod i)) 0.0 1.0 1.0)                   

         (glTranslated (* .05 (cos time)) 0.0 0.0)

         (glutWireCube 0.05))      

      (CGLFlushDrawable ctx)

      (CGLUnlockContext ctx)))



;; create opengl context

(define gl (gl:make-opengl))

(gl:open-opengl gl '(0 0 800 600))


;; get CGLContext from Impromptu OpenGL context

(define *cglctx* (objc:call (objc:call gl "openGLContext") "CGLContextObj"))


;; repeatedly call compiled code in loop

(define loop

   (lambda (beat) 

      (gltest *cglctx* beat)

      (callback (*metro* (+ beat (* 1/2 1/12))) 'loop (+ beat 1/12))))


(loop (*metro* 'get-beat 4))



COREFOUNDATION EXAMPLE


;; CoreFoundation Example

(definec cf-test

   (lambda (str)

      (CFStringCreateWithCString (CFAllocatorGetDefault) str 0)))


(define cfref-cptr (cf-test "My Test"))

;; cfstringref is toll free bridged to NSString

(define objc-ptr (objc:cptr->objc cfref-cptr))

;; print NSString

(print objc-ptr)


It is also possible to create your own pre-compiled libraries by calling definec with the *impc:compiler:print-raw-llvm* variable set to true.  This will output raw LLVM IR to the logview.  You can then copy this LLVM IR code into a source file (with an extension of ir) and place this ir file into the standard impromptu library location (~/Library/Application Support/Impromptu).  This IR code will then be loaded at startup - and as this skips the slow compilation phase is far preferable to calling definec on startup.  As a final step you must also call (definec-precomp <name>) for each function that you have precompiled.  These calls must be made from a standard scm library file to bind the Scheme wrapper function and initialise the runtime state of the compiled closure.  See example_ir_lib in the impromptu_2.5.dmg for a practical example.



Some Final Thoughts


There is still plenty of work to be done on the ICR.  This section outlines some of the most significant issues/gotchas that you might need to be aware of.


The compiler is currently very slow.  Not the output code emitted from the compiler - but the actual task of compilation itself.  This is something that I will fix in the future but at the moment compilation speed is not my primary concern so I'm trading slow compilation for ease/accuracy of development.  Slow compilation can result in delayed scheduled tasks if you are trying to compile code in a Scheme process that is also running temporal recursions and other time sensitive scheduled tasks (such as playing notes - drawing graphics etc.).  Although inconvenient this is not a disaster as you can safely compile from ANY Impromptu process.  This means, for example, that you can run a time sesative temporal recursion in the "primary process" and compile using the ICR in a secondary process - the "utility process" for example.  The primary process can still be using the compiled code as all compiled code is shared between Impromptu processes.


One important consideration for the ICR is that it is shared across all Impromptu system threads.  This includes all Scheme processes as well as the audio signal processing chain and OpenGL.  You must therefore take some responsibility for thread safety - if you try to access a function from two different Scheme processes at once you need to be aware of the concurrency issues involved.  In general I try to make it hard (but not impossible) for you to use ICR functions in more than one Scheme process at once.  So don't panic about this issue, but do keep it in the back of your mind.


Often when the type-inferencer get's stuck it reports back that it could not infer any types at all.  Unfortunately this is often not the case and solving a single type will in fact result in a successful compilation.  This needs to be fixed ... but for the moment you'll have to guess at which symbols need types adding them one by one.


There is currently no built-in editor support the ICR - meaning that there is no syntax-highlighting for the ICR - nor is there sys:help or context specific functional help.   These are all on the to-do list but will have to wait for a future release.


Next on my list for the new language is support for polymorphism with type classes, and also direct access to closure values.