Adding your own functions

This page describes the new structure for uLisp built-in functions introduced in uLisp Release 4.4, which allows you to include user functions in a separate file. The advantage of this is that you can extend uLisp without needing to edit the original uLisp source file, and you can update uLisp to a new release while keeping your same extensions file unaltered.

For the version of this document for earlier versions of uLisp see Adding your own functions pre 4.4.

You'll also find useful information in the section Converting between C and uLisp.

For examples of some useful extensions see:

Adding your own extensions to uLisp

Unlike in previous versions of uLisp, you can now add a separate Extensions File containing your own functions to uLisp, to extend the language or provide access to specific external hardware, and you only need to make one small change to the original source of uLisp.

All you need to do is:

  • Create a separate Extensions File. The name is not important, but you must give it a .ino extension; for example ulisp-extensions.ino.
  • Put it in the same folder as your uLisp source file.

You can use the example Extensions File as a template; see Example Extensions file.

In the Arduino IDE the ulisp-extensions.ino file is shown in a second tab in the Editor window.

  • In the main uLisp source file uncomment the line:
#define extensions

Otherwise you'll get the error:

redefinition of 'tbl_entry_t* table_table [2]'

The Extensions File

The Extensions File is divided into the following five sections:

  • Definitions, giving the definition of each function you want to add to uLisp.
  • Symbol names, a list of string definitions giving the names of the uLisp functions you are adding.
  • Documentation strings, a list of strings giving the documentation for each function.
  • Symbol lookup table, giving an entry for each function, referencing its definition, symbol name, documentation string, and a number specifying its type and number of parameters.
  • Table cross-reference functions; these need to be included to make your Extensions file accessible to uLisp.

These sections are explained in greater detail in the following sections by implementing a uLisp function called now that returns the time of day in hours, minutes, and seconds. For an explantion of how it works see The now example below.

Definitions

The Definitions section gives the C definition of each of the functions you want to add to uLisp.

// Definitions
object *fn_now (object *args, object *env) {
  (void) env;
  static unsigned long Offset;
  unsigned long now = millis()/1000;
  int nargs = listlength(args);

  // Set time
  if (nargs == 3) {
    Offset = (unsigned long)((checkinteger(first(args))*60 + checkinteger(second(args)))*60
      + checkinteger(third(args)) - now);
  } else if (nargs > 0) error2(PSTR("wrong number of arguments"));
  
  // Return time
  unsigned long secs = Offset + now;
  object *seconds = number(secs%60);
  object *minutes = number((secs/60)%60);
  object *hours = number((secs/3600)%24);
  return cons(hours, cons(minutes, cons(seconds, NULL)));
}

The convention used in the main uLisp source is to name each function with a prefix specifying its type, but this is optional:

Example Description
sp_defun Special forms
tf_progn Tail-recursive special forms
fn_equal Functions

In earlier versions of uLisp the functions had to be listed in a specific order in the lookup table; this is no longer necessary.

Note: Your C function must return a value of type object, such as nil, even if you don't need your Lisp function to return anything.

Symbol names

The Symbol names section defines the name of each Lisp function:

// Symbol names
const char stringnow[] PROGMEM = "now";

The name of each string should be a unique identifier. A good convention is to call it "string" followed by the name of the function; eg "stringnow".

Documentation strings

The Documentation strings section of the Extensions file optionally defines the documentation string for each function:

// Documentation strings
const char docnow[] PROGMEM = "(now [hh mm ss])\n"
"Sets the current time, or with no arguments returns the current time\n"
"as a list of three integers (hh mm ss).";
Again, the name of each string should be a unique identifier. A good convention is to call it "doc" followed by the name of the function; eg "docnow".

Symbol lookup table

The Symbol lookup table provides an extension to the array lookup_table[] in the main uLisp source. It should contain one entry for each function you are adding:

// Symbol lookup table
const tbl_entry_t lookup_table2[] PROGMEM = {
  { stringnow, fn_now, 0203, docnow },
};

In the above example there is only one entry because we are only defining one function, but the Extension file can define any number of functions, in which case there will be one entry in the Symbol lookup table for each function.

This contains four items:

  • The symbol name string for the function.
  • The function entry point.
  • The minmax code for the function; see below.
  • The documentation string for the function, or NULL if one is not required.

Minmax code

The minmax code is an 8-bit code, specified as a three digit octal number starting with a zero. The format of this code is as follows:

Minmax.gif

Giving the number in octal makes it easy to see the three fields.

fntype is an identifier to say what type of function it is:

  • sym. specifies a symbol, which would usually specify NULL for the function entry point.
  • sp specifies a special form. The arguments are not evaluated before being passed to the function.
  • tf specifies a tail-recursive special form. The arguments are not evaluated before being passed to the function, and the result is evaluated before being returned.
  • fn specifies a normal function. The arguments are evaluated before being passed to the function.

For the now function the minmax code 0203 specifies that it is a normal function that can take between 0 and 3 arguments.

Table cross-reference functions

These definitions need to be included verbatim at the end of the file to ensure that the extensions work correctly.

// Table cross-reference functions

tbl_entry_t *tables[] = {lookup_table, lookup_table2};
const int tablesizes[] = { arraysize(lookup_table), arraysize(lookup_table2) };

const tbl_entry_t *table (int n) {
  return tables[n];
}

int tablesize (int n) {
  return tablesizes[n];
}

The macro arraysize() is defined earlier as:

#define arraysize(x)       (sizeof(x) / sizeof(x[0]))

The now example

The definition of the now function illustrates several features of how Lisp functions are implemented in uLisp:

object *fn_now (object *args, object *env) {
  (void) env;
  static unsigned long Offset;
  unsigned long now = millis()/1000;
  int nargs = listlength(args);

  // Set time
  if (nargs == 3) {
    Offset = (unsigned long)((checkinteger(first(args))*60 + checkinteger(second(args)))*60
      + checkinteger(third(args)) - now);
  } else if (nargs > 0) error2(PSTR("wrong number of arguments"));
  
  // Return time
  unsigned long secs = Offset + now;
  object *seconds = number(secs%60);
  object *minutes = number((secs/60)%60);
  object *hours = number((secs/3600)%24);
  return cons(hours, cons(minutes, cons(seconds, NULL)));
}

Calculating the current value of millis

First the routine stores the current value of the millis() timer, converted to seconds, in the variable now:

unsigned long now = millis()/1000;

Checking the arguments

Usually the number of arguments supplied to a function is checked automatically, based on the value you specify for the minmax code, and you don't need to do any further checking.

However, the now function is unusual in that it can take only zero or three arguments, so we first check the number of arguments supplied to the function with:

int nargs = listlength(args);

Reading the arguments

If three arguments are supplied, these are used to set the current time by the block starting:

  if (nargs == 3) {

The C functions first(), second(), and third() are equivalent to the corresponding Lisp functions, and can be used to read each of the arguments from args, the list of arguments.

The function checkinteger() is applied to each argument to convert it to a C integer, with type checking.

The resulting numbers are then combined in the following expression to convert the time to seconds, and set the static variable Offset to the difference between that and the millis() timer:

    Offset = (unsigned long)((checkinteger(first(args))*60 + checkinteger(second(args)))*60
      + checkinteger(third(args)) - now);

Giving an error

If the number of arguments is not three or zero we call error2() to print an error message:

  } else if (nargs > 0) error2(PSTR("wrong number of arguments"));

Returning the current time

Finally, the function calculates the current time as Lisp integers in the variables seconds, minutes, and hours. It calls number() to convert each C integer to a Lisp integer:

  unsigned long secs = Offset + now;
  object *seconds = number(secs%60);
  object *minutes = number((secs/60)%60);
  object *hours = number((secs/3600)%24);

It then returns a list of these three values. The C function cons() is equivalent to the Lisp cons function, and is used to construct the list:

  return cons(hours, cons(minutes, cons(seconds, NULL)));

Using the now function

Once you've added this extension to uLisp you can use the now function to set or return the current time.

For example, to set the current time to 12:34:56 enter:

> (now 12 34 56)
(12 34 56)

You can now read the current time by entering:

> (now)
(12 35 01)

Acknowledgements

I'm grateful to Herbert Snorrason for suggesting ideas for restructuring uLisp incorporated into Release 4.4.