Saving and loading images

16th April 2017: This description has been updated to match uLisp 1.8.

The uLisp functions (save-image) and (load-image) allow you to save the Lisp workspace to non-volatile memory, and then load it at a later time, optionally automatically executing a specified function when the image has been loaded.

On the Arduino the EEPROM is used for saving images, but on other platforms the implementation depends on the non-volatile memory available on the processor. 

Introduction

For simplicity in the following descriptions I've omitted the additional code needed to save the symbol table and SymbolTop pointer.

The image is defined by the struct_image typedef:

typedef struct {
  unsigned int eval;
  unsigned int datasize;
  unsigned int globalenv;
  unsigned int gcstack;
  object data[IMAGEDATASIZE/4];
} struct_image;

This consists of the workspace data, plus the following additional image parameters:

  • image.eval, the address of a function to be run at startup.
  • image.datasize, the size of workspace to be saved, in 4-byte objects.
  • image.globalenv, the address of the global environment in the image.
  • image.gcstack, the address of the gcstack list in the image.

Compacting the image

In general the AVR processors have less EEPROM space than RAM space, so we need to compact the used objects in the workspace to ensure that they will fit in the EEPROM. This is achieved by calling compactimage():

int compactimage (object **arg) {
  markobject(tee);
  markobject(GlobalEnv);
  markobject(GCStack);
  object *firstfree = Workspace;
  while (marked(firstfree)) firstfree++;
  object *obj = &Workspace[WORKSPACESIZE-1];
  while (firstfree < obj) {
    if (marked(obj)) {
      car(firstfree) = car(obj);
      cdr(firstfree) = cdr(obj);
      unmark(obj);
      movepointer(obj, firstfree);
      if (GlobalEnv == obj) GlobalEnv = firstfree;
      if (GCStack == obj) GCStack = firstfree;
      if (*arg == obj) *arg = firstfree;
      while (marked(firstfree)) firstfree++;
    }
    obj--;
  }
  sweep();
  return firstfree - Workspace;
}

This first calls markobject() to mark all the used objects, and then moves each used object to the first free slot in the workspace. After each move it calls movepointer() to change any pointers affected by the move. Then finally it calls sweep() to collect up all the unused cells into free space.

The movepointer() function scans through the workspace for address cells matching the moved cell, and if found, updates them to point to the new position. Because the characters in a string could be confused for an address, it then traces down each of the strings restoring any that have been changed in the previous step:

void movepointer (object *from, object *to) {
  for (int i=0; i<WORKSPACESIZE; i++) {
    object *obj = &Workspace[i];
    unsigned int type = (obj->type) & MARKMASK;
    if (marked(obj) && (type >= STRING || type==ZERO)) {
      if (car(obj) == (object *)((unsigned int)from | MARKBIT)) 
        car(obj) = (object *)((unsigned int)to | MARKBIT);
      if (cdr(obj) == from) cdr(obj) = to;
    }
  }
  // Fix strings
  for (int i=0; i<WORKSPACESIZE; i++) {
    object *obj = &Workspace[i];
    if (marked(obj) && ((obj->type) & MARKMASK) == STRING) {
      obj = cdr(obj);
      while (obj != NULL) {
        if (cdr(obj) == to) cdr(obj) = from;
        obj = (object *)((unsigned int)(car(obj)) & MARKMASK);
      }
    }
  }
}

Saving and loading an image

The saveimage() function then simply saves the image and image parameters to EEPROM:

int saveimage (object *arg) {
  unsigned int imagesize = compactimage(&arg);
  // Save to EEPROM
  if (imagesize > IMAGEDATASIZE) {
    pfstring(F("Error: Image size too large: "));
    pint(imagesize); pln();
    GCStack = NULL;
    longjmp(exception, 1);
  }
  eeprom_update_word(&image.datasize, imagesize);
  eeprom_update_word(&image.eval, (unsigned int)arg);
  eeprom_update_word(&image.globalenv, (unsigned int)GlobalEnv);
  eeprom_update_word(&image.gcstack, (unsigned int)GCStack);
  eeprom_update_block(Workspace, image.data, imagesize*4);
  return imagesize;
}

The loadimage() function loads the image and image parameters from EEPROM and performs a garbage-collect to reset the free space after the load:

int loadimage () {
  unsigned int imagesize = eeprom_read_word(&image.datasize);
  if (imagesize == 0 || imagesize == 0xFFFF) error(F("No saved image"));
  GlobalEnv = (object *)eeprom_read_word(&image.globalenv);
  GCStack = (object *)eeprom_read_word(&image.gcstack);
  eeprom_read_block(Workspace, image.data, imagesize*4);
  gc(NULL, NULL);
  return imagesize;
}

Note that a saved image will no longer work if uLisp has been edited and recompiled, and the position of the workspace or function lookup table has changed. If you need to disable autorun because an invalid image gets loaded on reset, comment out the line:

#define resetautorun

at the start of the uLisp source and recompile it.