Adding your own functions

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

Update

16th February 2021: This description has been updated to match Version 3.5 of uLisp.

How functions are implemented

As an example of how a normal function is implemented in C, let's have a look at a typical function, fn_add, which handles the Lisp "+" operator:

object *fn_add (object *args, object *env) {
  (void) env;
  int result = 0;
  while (args != NULL) {
    int temp = checkinteger(ADD, car(args));
    result = result + temp;
    args = cdr(args);
  }
  return number(result);
}

For simplicity I've omitted the overflow checking code.

This function will get called when you evaluate a form such as:

(+ (* 2 3) (* 4 5) (* 6 7))

First, each of the arguments is evaluated, giving:

(+ 6 20 42)

The first item in this list is looked up to give the function entry point, fn_add, and this is then called with args pointing to the cdr of this list:

(6 20 42)

If the function took a fixed list of arguments you could access each argument with statements such as:

arg1 = first(args);
arg2 = second(args);

However, in this case the function will take a variable number of arguments, so we need to iterate along the list of arguments with a loop until the args list is empty (NULL):

while (args != NULL) {
  // do something
  args = cdr(args);
}

In the case of fn_add we start with the integer result set to 0, then repeatedly get the first item in the args list, with car(args), call checkinteger() to convert it to an integer, and add it to result.

The function checkinteger() also checks the argument is an integer and reports an error if it isn't. The first argument, ADD, allows the name of the function to be displayed in the error message.

The second argument to each function, env, contains the current environment: an association list of local variables and their values. It is used by functions that call eval, but is not required by fn_add.

Adding your own function to uLisp

It's quite easy to add your own functions to uLisp, to extend the language or provide access to specific external hardware. Proceed as follows:

  • After the comment
// Insert your own function definitions here

write the C definition of your function, which should have a declaration such as:

object *fn_myfun (object *args, object *env) {
  • Add a string defining the name of the function after the comment:
// Insert your own function names here

For example:

const char mystring1[] PROGMEM = "myfun";

The function name doesn't have to match the C function name; it just has to be distinct from any existing function name. It can contain arbitrary characters, apart from open bracket, close bracket, single quote, or dot, and must not start with a digit or minus sign.

  • Add a table entry at the end of lookup_table[] after the comment:
// Insert your own table entries here
  • The entry should specify the name of the string you defined, the C function name, and a byte of two nibbles specifying the minimum and maximum permitted number of arguments; for example, if your function takes exactly one argument:
{ mystring1, fn_myfun, 0x11 },
  • Add an uppercase symbol representing your function in the enum function definition near the beginning of the uLisp source file. Your new symbol should go just after USERFUNCTIONS in the list; for example:
enum function { BRA, KET, QUO, ... USERFUNCTIONS, MYFUN, ENDFUNCTIONS };
  • Now recompile uLisp and your function should be available in the listener.

Providing error handling

You can provide error handling by calling error2. For example:

if (addr < 0) error2(MYFUN, PSTR("invalid address"));

This will display:

Error: 'myfun' invalid address

Example

Here's an example, inspired by a question from Ronny Suy in the uLisp forum. It describes how to add peek and poke functions to uLisp, like the BASIC functions to read the contents of an address, and write a value into an address.

First define the functions:

// Insert your own function definitions here

object *fn_peek (object *args, object *env) {
  (void) env;
  int addr = checkinteger(PEEK, first(args));
  return number(*(int *)addr);
}

object *fn_poke (object *args, object *env) {
  (void) env;
  int addr = checkinteger(POKE, first(args));
  object *val = second(args);
  *(int *)addr = checkinteger(POKE, val);
  return val;
}

Note that poke returns the value you gave it - it doesn't read back the contents of the address.

Next add their names on the end of the existing list:

// Insert your own function names here
const char mystring1[] PROGMEM = "peek";
const char mystring2[] PROGMEM = "poke";

Then add their table entries onto the end of the existing table:

// Insert your own table entries here
  { mystring1, fn_peek, 0x11 },
  { mystring2, fn_poke, 0x22 },
};

Finally add their symbols onto the end of the enum function list:

USERFUNCTIONS, PEEK, POKE, ENDFUNCTIONS };

Trying it out:

> (peek #x260)
0

> (poke #x260 123)
123

> (peek #x260)
123
Note that I had taken care to check that the memory location #x260 was safe to write to!