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).";
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:
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.