Home |  Overview |  Registers & Memory |  Interrupts |  Internal Representations |  Programming

A Scheme Interpreter for ARM Microcontrollers: Implementation

SourceForge.net Logo


Armpit Scheme is written in 32-bit ARM assembly language (not THUMB) for the ARM7TDMI core (ARMv4T architecture). The relevant assembly language is summarized in Quick Reference Cards here and here, and the core's technical reference (eg. operating modes, stack usage, ...) is available here.

The Armpit Scheme source code is organized into 3 major sections and several subsections as follows:



      III.E. COMMON COMPONENTS OF USB ISR (lpc_2148 and at91_sam7)

The scheme language implemented in section II of the source code is presented on the Armpit Scheme Language web page.

Armpit Scheme runs the MCU in ARM User Mode (not System Mode, not Supervisor Mode). All interrupts processed by the Interrupt Service Routines (ISRs) in sections I and III of the source code are categorized as IRQ (not FIQ).


Register and Memory Usage:

During normal program execution (outside of interrupts) the 16 ARM User Mode registers are used by Armpit Scheme as follows:

    r0    pointer to the next free memory cell (word)
    r1    scheme value work register
    r2    scheme value work register
    r3    scheme value work register
    r4    scheme value work register
    r5    scheme value work register
    r6    raw value work register (not garbage collected)
    r7    raw value work register (not garbage collected)
    r8    pointer to top-level environment
    r9    pointer to object array
    r10   pointer to data stack
    r11   pointer to return stack
    r12   pointer to global scheme vector:
            [timer0-callback, timer1-callback, i2c0-data, i2c1-data, current input port, current output port]
    r13   system stack pointer (ARM sp)
    r14   system link return (ARM lr)
    r15   system program counter (ARM pc)

The value in r0 is updated by the memory management code of subsection I.E., each time new memory is allocated on the heap, and during garbage collection. The data stack and return stack are used for function entry and exit. These stacks are treated functionally in most of the code (i.e. they are not side-effected) such that pointers to their respective tops can be used to define continuations. The system stack is used (exclusively) for interrupts and during garbage collection (rather than to stack return addresses during ordinary Scheme function calls).

Registers r1 to r5 temporarily store scheme values, including funtion input arguments obtained from the data stack, output values returned to the data stack, return addresses obtained from the return stack and return addresses to be pushed onto the return stack. Registers r6 and r7 temporarily store non-scheme values like raw integers used during mathematical operations and raw ASCII characters used during input/output operations.

The top-level environment, object array, data stack and return stack are scheme lists whose starting addresses are kept in registers r8 to r11. They consist of cons cells that each occupy 8 contiguous bytes of RAM (2 contiguous words). The first word of the cons cell contains the car and the second word contains the cdr of the cell. For example, in ARM assembly language, the car of the data stack is refered to by [r10] and the cdr of the data stack is refered to by [r10, #4]. The bottom of this stack, and of the return stack, consist of a null circular cons cell (a cell whose car is null and whose cdr points to its car), stored in FLASH, and labeled stkbtm: in the source code.

Armpit Scheme uses a stop-and-copy garbage collector that is automatically called by memory management functions (code subsection I.E.) when allocation of new memory would cause the heap to extend beyond its allowable top address. A memory management function that performs garbage collection (gc) uses 64 bytes of User Mode System stack space. The root object used for gc is a scheme vector that contains a copy of the contents of registers r1 to r5 and r8 to r12 (i.e. all the scheme objects accessible to the MCU). A memory management function that needs to initiate gc (eg. pcons, malloc, fremem) first pushes its lr (r14) (4 bytes) onto the system stack and then performs a branch-and-link (bl) to the gc: label to initiate garbage collection. When gc is initiated, the contents of non-scheme registers r6, r7 and r14 (to return from gc) (12 bytes) are pushed onto the system stack, followed by the root object (48 bytes = 40 bytes of data + the 8 byte vector header). The updated root object, and the original r6, r7 and r14, are popped off the system stack when gc is complete. The popped r14 is used to return to the memory management function that called gc. The memory management function then pops its own lr (r14) from the system stack so that it can return to the code that called it.

The following diagram illustrates the MCU memory map during Armpit Scheme execution. The map is fixed (it does not change during program execution) but differs slightly for each MCU or board according to its features (eg. separate USB RAM area, external RAM). The assembled source starts at address 0, the start of FLASH, and is followed by the user-defined Scheme startup code (if any) (written using Armpit's (flash) function) also in FLASH. The RAM is occupied by various buffers in its lower portions, followed by the two heaps required by the stop-and-copy gc approach, and then the User Mode and IRQ system stacks. The size of the heaps varies from approximately 3.5 kB (each) on the LPC2131 which has 8 kB of RAM to approximately 512 kB (each) on the H2214 board that has 1 MB of off-chip RAM. The top of the MCU memory is occupied by hardware registers that control peripherals and overall operation of the device:

      ----ADDRESS: 4GB, Top of Addressable Area-----
              **no FLASH or RAM here**
      ---ADDRESS: Top of System Peripherals Area----
      --ADDRESS: Bottom of System Peripherals Area--
              **no FLASH or RAM here**
      ----------ADDRESS: Top of RAM-----------------
            IRQ MODE SYSTEM STACK (88 bytes)
         USER MODE SYSTEM STACK (68 or 72 bytes)
               HEAP1 (3.5 kB to 512 kB)
               HEAP0 (3.5 kB to 512 kB)
             READBUFFER  (mostly < 1 kB)
             I2C_BUFFERS (2 x 20 bytes)
           USB_BUFFERS (4 bytes to few kB)
      ---------ADDRESS: Bottom of RAM---------------
              **no FLASH or RAM here**
      ----------ADDRESS: Top of FLASH---------------
                  **unused FLASH**
                  **unused FLASH**
      -------ADDRESS: 0, Bottom of FLASH------------

It is notable that Armpit's READBUFFER is a single memory zone and is shared by UART0, UART1 and USB interfaces such that only one of these devices should be active in character reception at any given time (or input will become mangled). This design decision was made such that the maximum amount of heap space is available to scheme programs and is most beneficial to the smallest MCUs. Armpit Scheme's "read" functions (read-char, peek-char, read (if reading from a uart or usb)) obtain their characters from the READBUFFER and update it (i.e. remove a character or a scheme expression from it) in accordance with their definition.


Interrupt Service:

Two Interrupt Service Routines (ISR) are defined in Armpit Scheme. The generic ISR with label genisr: (code subsection I.D.) processes uart, timer and I2C interrupts (for I2C interrupts, genisr branches to i2cisr: in III.B). The USB ISR with label usbisr: (code subsection III.E) processes USB interrupts. It is notable that: i) gc can occur during ISR execution if the ISR allocates new heap memory (in which case gc uses the IRQ stack); ii) the timer ISRs can branch to user-defined scheme functions stored in the global system vector (r12), and; iii) both timer and I2C interrupts are disabled (at the core) during the execution of memory management fuctions. If any, the memory management (and gc), that occur during an ISR, is performed before any scheme object (gc-eable object) has been pushed onto the IRQ stack. The ISRs for uart, timer, I2C and USB use up to 32, 84, 80 and 52 bytes of IRQ System Stack space, respectively, including gc (if any).

UART0 is the default I/O device used by Armpit Scheme when it is not connected to a valid USB line. In this case, UART0 interrupts are set to fire for each character received by the interface. The uart ISR reads the received character from the uart's receive-hold register and stores it at the next available location in Armpit's READBUFFER, or otherwise updates this buffer appropriately when a backspace or "Enter" is received. The ISR also echoes received characters to the uart's Tx line.

The Armpit Scheme timer ISR is designed to allow task-switching. When invoked by a timer interrupt, it saves the current computation (MCU registers) in a gc-eable vector and a non-gc-eable vector that form a context for resuming the interrupted computation. If no procedural scheme timer-callback is found in the global vector r12, the ISR simply restores this context to continue the otherwise interrupted process. If a procedural scheme timer-callback is found in r12, then the ISR pushes the context onto a fresh data-stack and pushes rsrctx (the label of the function used to restore a saved context) onto a fresh return-stack, thereby setting the continuation of pending computations to be the resumption of the interrupted process. It then applies the callback to the timer interrupt status (stored as a scheme integer) in the callback's closure environment, and with the fresh stacks as data and return stacks. The callback can perform simple operations and exit, in which case its exit triggers resumption of the interrupted process (its continuation), or it can capture its continuation with call/cc, push it onto a process queue and resume a different interrupted process from that queue (for preemptive multitasking). The user stores a callback in r12 using the Armpit Scheme extended scheme write function, simply write the callback to the timer0-or-1 port with the optional argument {register} set to #x010000 (see Level 1 Library and Addendum in the Language reference web page). The callback can be read from that location using the read funtion in a similar way.

The Armpit Scheme I2C ISR performs interrupt-driven sending and receiving of raw bytes and scheme objects (sent and received bytewise) through I2C channels. As of version 00.0036, it is available only on LPC2000 MCUs. I2C channel status, data ready, number of bytes to send/receive, number of bytes sent/received and up to 4 bytes to-send or up to 8 bytes that-have-been-received are stored in small buffers at or near RAM bottom. These small buffers are not part of the heap and are not gc-ed. Larger objects to send, or received, are stored in the global scheme vector in r12 such that they remain valid across gc operations (r12 is part of the gc root). The I2C ISR performs memory allocation when it receives a scheme object larger than 4 bytes (eg. a string or a packed object) from a remote device. The address of memory allocated for this oversized object is then stored in r12.

USB interrupts are enabled in Armpit Scheme when the MCU is connected to an active USB line. During character reception, the USB ISR operates similarly to the uart ISR described above but obtains its input characters from the USB line. The USB system, however, also uses a WRITEBUFFER to send characters out through USB. Scheme "write" functions targeted to the USB port write the relevant external representations to the USB WRITEBUFFER and the ISR sends the contents of this buffer out through the USB line when the appropriate request (which generates an interrupt in Armpit Scheme) is received from the USB Host. The echo of characters received via USB is handled similarly. The ISR further handles standard, interrupt-driven, USB management tasks, such as device enumeration.


Internal Representations:

Armpit Scheme uses the lower bits of data objects to identify their type. The tagging is adaptive and can consist of 2, 4 or 8 bits. Two-bit tags are used for addresses, integers and floats. The type of an object can be recognized by inspecting its two least significant bits as follows:

      00 -> address
      01 -> integer
      10 -> float
      11 -> other object

Addresses are aligned to word boundaries and hence natively have their lowest two bits as 00 in the 32-bit Armpit system. Integers are stored as a raw value in the 30-bits above their type tag, using two's complement. A raw integer is shifted left by 2 bits and orred with the #b01 type tag to make a scheme integer. The converse operation is performed by an arthmetic shift, two bits to the right, to preserve the sign and value of the raw integer. Floats are represented using a shortened form of the IEEE-754 32-bit standard, namely the lower two bits of the mantissa are replaced by the type tag #b10. The following illustrates these internal representations, with uppercase letters representing hexadecimal values (X if arbitrary), lower case letters representing bits, and numerical digits representing actual values (s, e and m stand for sign, exponent and mantissa):

      XXXXXXXbb00 -> address, aligned to word boundary
      XXXXXXXbb01 -> integer
      s-eeeeeeee-mmmmmmmmmmmmmmmmmmmmm-10 -> float

Four-bit tags are used by the garbage collector, and other functions, to identify sized objects and broken hearts. The lower 2 bits of these tags are always #b11 to differentiate them from 2-bit tags. Two of the 4-bit tags are used to separate sized objects into two categories: gc-eable and non gc-eable. Gc-eable sized objects are those whose contents consist entirely of scheme object that need to be considered during gc operations. This category includes scheme vectors. Non gc-eable sized objects are those containing non-scheme data that should not be scanned for pointers during gc. This category includes strings, symbols and packed objects. The following 4-bit tags are used to identify these scheme objects:

      0011 -> non gc-eable sized object
      0111 -> broken-heart
      1011 -> gc-eable sized
      1111 -> other object

The 4-bit tag used for broken-hearts is also their full internal representation (all their other bits are zero). For sized objects, the 4-bit tags are supplemented with a 4-bit subtype that differentiates between vectors, strings, symbols and packed objects. The 4-bit tags and 4-bit subtypes form byte tags as sollows:

      0001-0011 = #x13 -> symbol
      0101-0011 = #x53 -> string
      1001-0011 = #x93 -> packed-object
      0001-1011 = #x2B -> vector

Each sized object is stored in a set of contiguous memory locations according to its size. The data stored in the object is preceded by a multi-word header designed to support future multi-dimensional extensions. The format of the header is as follows:

      byte  at offset 0:     object tag
      byte  at offset 1:     number of bytes per item in this object, stored as lowest 8-bits of a scheme integer
      bytes at offset 2,3:   number of dimensions of this object, stored as lowest 16-bits of a scheme integer
      word  at offset 4:     size of object along dimension 1, stored as scheme integer (4 bytes)
      word  at offset 8:     size of object along dimension 2, stored as scheme integer (4 bytes)
      ...                    ...
      word  at offset 4*dim: size of object along last dimension, stored as scheme integer (4 bytes)

Sized objects currently recognized by Armpit Scheme are one-dimensional and hence have a 2-word header where the bytes at offset 2,3 are #x0005. Symbols, strings, packed-objects use a single byte per data item they store (eg. ASCII chars or raw byte values) and vectors use a single word per data item that they store which enables them to store arbitrary scheme objects. Considering the foregoing, the full internal representation of implemented sized objects becomes:

      ------------------ --------------------- --------------------- --------------------- ---------------------
      object:            symbol                string                packed-object         vector
      ------------------ --------------------- --------------------- --------------------- ---------------------
      word at offset  0: #x00050513            #x00050553            #x00050593            #x0005112B
      word at offset  4: num-chars(scheme-int) num-chars(scheme-int) num-bytes(scheme-int) num-words(scheme-int)
      word at offset  8: ascii-chars-0123      ascii-chars-0123      bytes-0123            scheme-object-1
      word at offset 12: ascii-chars-4567      ascii-chars-4567      bytes-4567            scheme-object-2
      ...                ...                   ...                   ...                   ...
      ------------------ --------------------- --------------------- --------------------- ---------------------

All other objects in Armpit Scheme have an 8-bit identifier. This includes the constants null, #t and #f that are stored as just a tag as follows:

      0000009F -> '()
      000000AF -> #t
      000000BF -> #f

Scheme characters are represented with the 8-bit tag #x3F and the ascii value of the character at offset 1 within the character word:

      0000CC3F -> character, CC is the character's ASCII code

Scheme variables have the 8-bit tag #x6F. Bytes at offsets 1 and 2 represent the unique numerical ID of the variable, assigned to it by string->symbol (eg. as used by read), and stored as a raw 16-bit integer. The upper byte of the variable stores the ID of the MCU on which the variable was created, or zero if it is an implementation variable. The MCU ID is included in the variable to differentiate user-defined variables from implementation variables and to enable multiprocessing where the MCU may receive variables defined on another MCU that must not be confused with variables that have the same 16-bit numerical IDs but that were defined on the local MCU. The MCU ID used in the internal representation of variables is that defined for I2C communication over the i2c0 interface and is defined in preprocessor declarations at the top of the Armpit Scheme source code (where a user may select MCU family, board and usb options). It can be modified by writing to the appropriate I2C register of the MCU (I2C0ADR on LPC2000 devices). Armpit Scheme variables are represented internally as words of the form:

      MC-VRID-6F -> variable, MC is the MCU ID or 0, VRID is the 16-bit variable ID

Syntax variables are represented internally by the 8-bit tag #x2F and a 16-bit numerical (VRID) as:

      00-VRID-2F -> syntax

Primitive procedure objects are represented as one cons cell (8 contiguous memory bytes) whose car consists of the 8-bit tag #xFF and whose cdr is the address of the label of the primitive's assembly language code in FLASH:

      primitive    <-  (#xFF . address-of-primitive's-code) == [#x000000FF address-of-primitive's-code]

Macros are represented internally as cons cells whose car is the tag #x8F and whose cdr is the macro-body. The macro-body is a regular list of literals and transformers, all of which are represented internally as regular lists (without tags):

      macro        <-  (#x8F . macro-body)
      macro-body   <-  (literals transformer1 transformer2 ...)
      literals     <-  (variable1 variable2 ...)
      transformer  <-  (pattern template)

Macros are returned by the scheme function syntax-rules: (syntax-rules literals transformer1 transformer2 ...). Their macro-body can be inspected simply by using the cdr function, for example: typing (cdr quasiquote) at the rep displays the literals and transformers used to define quasiquote in Armpit Scheme.

Scheme promises are represented internally as lists of 4 elements whose car is the tag #xCF. They are generated by the scheme syntax function delay, for example (delay expression). The internal representation list includes the delayed expression and the environment in which it is to be evaluated:

      promise      <-  (#xCF environment () expression)

Scheme continuations are represented internally as lists of 3 items with the tag #xDF as car. They are built by the scheme function call/cc. The continuation's list contains the address of the relevant data and address stacks needed to return locally or non-locally from the current computation. These stacks are functional in Armpit Scheme, that is, they are not side-effected by code:

      continuation <-  (#xDF address-of-return-stack address-of-data-stack)

Scheme closures are represented internally as lists of 4 elements whose car is the tag #xEF. They are built by the scheme syntax function lambda as in: (lambda (variable1 variable2 ...) body). The closure's internal representation list includes the environment in which the closure is defined, its list of input variables and its body:

      procedure    <-  (#xEF environment (variable1 variable2 ...) body)

For efficiency, the implementation environment and object array (obarray) are represented slightly differently from the dynamic obarray and environments resulting from running code. The dynamic obarray is a list of bindings between variables and their symbols where each binding is a cons cell with the variable as cdr and its symbol's address as car:

      obarray      <-  (obinding1 obinding2 ...)
      obinding     <-  (address-of-var's-symbol . variable)

Dynamic environments are represented as lists of frames where each frame is a list of bindings between variables and their values. As with the obarray, the bindings are implemented as cons cells whose car is the variable and the cdr is its value (a scheme object).

     environment   <- (frame1 frame2 ...)
     frame         <- (ebinding1 ebinding2 ...)
     ebinding      <- (variable . value)

In contrast to the above dynamic objects, the implementation environment and obarray are interwoven within a regularly spaced, contiguous block of FLASH (in subsection II.H. of the source). This is such that implementation variables can be given implicit (rather than explicit) 16-bit VRIDs that are based on their position within the block. This approach simplifies the addition and removal of built-in functions to/from Armpit Scheme as examplified in the next section. There is no need to manually compute or re-compute VRIDs and a much reduced need for manually linking environment and obarray items via car/cdr addresses.

The implementation environment/obarray fuses the obarray's symbol-variable binding cons cell with the environment's variable-value binding cons cell, and introduces the implicit ID concept, to obtain, for a given object:

      (address-of-var's-symbol . variable) + (variable . value)
      -> [address-of-var's-symbol   variable-tag-or-syntax-tag   value]

This fused dual binding maintains the relative positions of the car-cdr pairs found in bindings of the dynamic obarray and environments. In the implementation environment/obarray, the value found in the third word position of the fused dual-binding is either an immediate (for constants), a primitive or a macro. The latter are both represented as cons cells and hence the whole implementation/obarray entry for a given variable can be specified in a single 4-word contiguous block of memory which is the representation selected in Armpit Scheme. The corresponding 4-word implementation environment/obarray entries for constants (eg. null, #t, #f), procedures (eg. eq?, number?, +, cons, car), syntax procedures (eg. quote, lambda, if) and macros (eg. quasiquote) are as follows:

      constant  <- [address-of-var's-symbol variable-tag immediate-scheme-value 0                          ]
      procedure <- [address-of-var's-symbol variable-tag primitive-tag          address-of-procedure's-code]
      syntax    <- [address-of-var's-symbol syntax-tag   primitive-tag          address-of-syntax's-code   ]
      macro     <- [address-of-var's-symbol variable-tag macro-tag              address-of-macro-body      ]

The implementation environment/obarray consists of a contiguous sequence of such entries, starting at label scmenv: in the source code and stored in FLASH memory. The sequence ends with an entry consisting of 4 scheme-null words. Since each entry occupies 16 bytes of FLASH (4 words), the implicit VRIDs of built-in variables and syntax variables can be calculated as the memory address at which the entry starts, minus the address of label scmenv, divided by 16. Scanning of the implementation environment/obarray in the assembly source code is correspondingly performed with a pacing step size of 16 bytes (cf. string->symbol and symbol->string in subsection II.A of the source).


Programming New Functions:

Adding new functions to the Armpit Scheme top-level consists of 3 steps: 1) add assembly language code for the function, preferably in the Level 2 Addendum (code subsection II.G.); 2) add a link to the function and its symbol to the implementation environment (code subsection II.H), and; 3) add the function's symbol to the external representations (code subsection II.I). These steps are examplified below for the addition of a function named "revenu" (a kind of "backwards" enumeration used here for simplicity of code).

The target new function, revenu, takes either a positive integer count and an ending integer or a positive integer count, an integer step and an ending integer as input values, and returns a list of "count" integers that ends with the ending integer, and decrements by the step (or 1 as default). Once the function has been added to the code and the code has been reassembled and uploaded to the MCU, it will be possible to use this new function at top-level and perform operations such as:

30824 armpit> revenu
30824 armpit> (revenu 3 7)
(9 8 7)
30824 armpit> (revenu 5 3 2)
(14 11 8 5 2)
30824 armpit> (revenu 4 -10 100)
(70 80 90 100)

Much like the revenu function, the implementation example is easiest to present "backwards", starting with the external representation (step 3). The external representation of revenu is the symbol of 6 characters: r, e, v, e, n, and u. The symbol, with its tag word 0x050513 (05 = 1 dimensional sized object, 05 = 1 byte per item, 13 = symbol) and its number of items 0x19 (i.e. 6 expressed in Armpit Scheme's internal representation of integers) are placed in code subsection II.I, for example, right after the external representation of the Level 2 Addendum function "pack", at the new label rvnu00:, such that the source code in that area becomes:

@ pack/unpack:			LEVEL 2 ADDENDUM
pack00:	.word	0x050513, 0x11	@ symbol (01 dim, 01 byte/item, 13=symbol_tag), number of bytes
	.ascii	"pack"
	.balign 4
@ revenu:			LEVEL 2 ADDENDUM (uses 2 words + 6 bytes + 2 alignment bytes = 16 bytes of FLASH)
rvnu00:	.word	0x050513, 0x19	@ symbol (01 dim, 01 byte/item, 13=symbol_tag), number of bytes
	.ascii	"revenu"
	.balign 4

Next (step 2), the implementation environment is updated with a link to the external representation of the new function and to its code. The assembly language code of the function will be placed at label revenu: and the function is tagged as a primitive procedure accessed from the variable whose symbol is the external representation of revenu stored at label rvnu00. This specification is inserted into the Level 2 Addendum part of the implementation environment (code subsection II.H), right after the entry for pack such that the source code in that area becomes:

@ pack/unpack:			LEVEL 2 ADDENDUM

		.word	pack00, variable_tag,	primitiv_proc,	pack		@ pack		{variable}
@ revenu:			LEVEL 2 ADDENDUM (uses 4 words = 16 bytes of FLASH)

		.word	rvnu00, variable_tag,	primitiv_proc,	revenu		@ revenu	{variable}

Finally (step 1), the assembly language code of the function revenu is developed and placed in the source code at label revenu:, for example in subsection II.G., right after pack: such that the code in that area becomes:

@  pack/unpack:			pack

@ Addendum: pack/unpack

pack:	@ (pack object)			  ((object) ...) -> (packed-object ...)
	@ r3  <- target offset,  r4  <- source list,  r5  <- relocation list
	ldr	r1,  [r10]		@ r1  <- (object)
	mov	r2,  r10		@ r2  <- ((object) ...)
	ldr	r10, [r10, #4]		@ (address_of_target_sized_object ...)
	b	pack_9

@ Addendum: revenu

revenu:	@ function usage in scheme:     (revenu count end|{step} {end})
        @ data stack on entry:          ((count end|{step} {end}) ...)
        @ data stack on exit:           ((end+count*step ... end+step end) ...)
	@ FLASH memory usage:		19 code lines = 19 words = 76 bytes
	bl	stdfen		        @ r1  <- count, r2  <- end/step, r3  <- {end}, ds  <- (...)     Line  1
	sub	r5,  r1, #4		@ r5  <- count - 1	    (from r1, scheme integer)           Line  2
	teq     r3,  #scheme_null       @ was step not specified (i.e. {end} = '()) ?                   Line  3
        moveq	r1,  r2	                @       if so,  r1  <- end  (from r2, scheme integer)           Line  4
        moveq   r4,  #5                 @	if so,  r4  <- 1    (default step, scheme integer)	Line  5
	movne	r1,  r3			@	if not, r1  <- end  (from r3, scheme integer)		Line  6
	movne	r4,  r2			@	if not, r4  <- step (from r2, scheme integer)		Line  7
	mov     r2,  #scheme_null	@ r2  <- '()                                                    Line  8
	mov	r3,  #int_tag           @ r3  <- integer tag to convert raw ints to scheme ints		Line  9
rvnulp:	bl      pcons                   @ r1  <- (... end) == updated function result                   Line 10
        teq	r5,  #int_tag		@ is count = 0 (i.e. are we done) ? (0 as scheme int = int_tag) Line 11
        beq	stdfxt			@       if so,  exit with r1 (push r1 on ds, pop rs)            Line 12
	mov	r2,  r1			@ r2  <- (... end) == current result                            Line 13
	ldr	r1,  [r2]		@ r1  <- latest value consed to result (scheme integer)		Line 14
	mov	r7,  r1, ASR #2		@ r7  <- latest value consed to result (raw integer)		Line 15
	add	r7,  r7, r4, ASR #2	@ r7  <- next value to cons to result, (raw integer)		Line 16
	orr	r1,  r3, r7, LSL #2	@ r1  <- next value to cons to result, as scheme integer	Line 17
	sub	r5,  r5, #4		@ r5  <- updated count (scheme int)                             Line 18
	b       rvnulp                  @ jump to add next item and/or exit                             Line 19

The function receives its input arguments (scheme objects) via the list found on top of the data stack. The continuation for the function consists of the return stack and the cdr of this function-entry data stack. When the function exits, it will pass its return value, contained in r1, to its continuation by consing it to the cdr of the entry data stack and setting the result as the new data stack. The continuation will then be invoked by branching to the label at the top of the return stack which will be popped. These standard function entry and exit are performed by branching to labels stdfen: and stdfxt: defined in subsection I.F. of the Armpit Scheme source code.

In Line 1 of the revenu code, the list of input arguments is popped from the data stack and the arguments are extracted and stored into registers r1 to r3 by the standard function entry function (called via branch-and-link). The number of items to cons onto the return list, in additon to the end value, is computed from count (scheme integer in r1) and stored in r5 for later use on Line 2 (scheme ints are shifted left by 2 bits relative to raw ints and therefore adding 4 to them is equivalent to adding 1 to a raw int. The same holds for subtraction if numbers are and remain positive or 0). Line 3 tests to see if 2 or 3 input arguments were provided to the function -- if only 2 were provided then stdfen will have set r3 to '(). The next 4 lines store the end value (scheme integer) in r1 (for later use in pcons) and store the step in r4 (scheme integer). The scheme null '() is then stored in r2 and the integer tag (#b01) is stored in r3, for later use (lines 8 and 9).

The main code loop starts on Line 10 with a call to pcons that creates a new cons cell on the heap with r2 (initially the null list) as cdr and r1 (initially the end value) as car and returns the address of this new cons cell in r1. This function is found at label pcons: in code subsection I.E. (Memory Management) and its operation modifies the contents of scheme registers r1 and r2, and of raw registers r6 and r7. After Line 10 is executed, we find in r1 the first acceptable result of function revenu, namely the list (end). Line 11 tests whether more integers should be consed to the front of that list. If not (i.e. if count, stored in r5 as a scheme integer, is zero) the function returns by branching to the standard function exit which invokes its continuation with r1 at the top of the new data stack. If count is not zero then the current result (in r1) is moved to r2 as it will be the cdr of the next cons cell to be built (Line 13). Line 14 extracts the last consed value (the car) from the result list and stores it in r1. In Lines 15 to 17, the next integer to be consed to the front of the result list is calculated from the last value (in r1), the step (in r4), and the integer tag (in r3), and is then stored in r1 to serve as car of the next cons cell to be built by pcons. These operations follow the Armpit Scheme requirement of storing only gc-eable scheme objects in registers r1-r5 and non-gc-eable raw objects in registers r6 and r7. The count of the remaining number of integers to cons to the result list is updated on Line 18 and the code jumps back to repeat consing, testing and updating operations on Line 19.

Last updated February 8, 2007