Embedding - misaure/lispel GitHub Wiki

Initializing the Interpreter

To make use of the Lispel interpreter from inside your C++ application you will have to include the header file lispel.hh. If you have installed Lispel in the standard way, just add the following line to your program:

#include <lispel/lispel.hh>

After including the main Lispel header file you can create an interpreter instance and add the various command sets you want to be useable from scripts:

 Interpreter interp;
 try {
   addCoreCommands( interp);
   addListCommands( interp);
   addMathCommands( interp);
   interp.repl( &cin);
 }
 catch( lispel_exception e) {
   cerr << "\nexception caught:"  << endl;
   cerr  << e  << endl;
 }

The first line creates an interpreter which is built from a predefined set of default components and which creates a heap with some default size defined in the Lispel sources (cf. interpreter.cc). Initially, there won't be any functions or special forms defined in the interpreter. You should at least call the function addCoreCommands with a interpreter instance as the only argument to add the most basic functions and special forms. The language reference tells you what will be added by this function. The core functions are assumed to be safe in that they don't allow scripts to access any sort of files or environment variables.

The functions addListCommands and addMathCommands add functions related to list and math functions which aren't assumed to be part of the systems core to the interpreter. These functions are also safe. As of version 0.0.1 of Lispel, all of these command sets are very incomplete and there are many more command sets missing. But the commands already should suffice to implement more or less complex configuration files.

The final part of the code sample catches any exceptions thrown during installation of the basic Lispel commands. The class lispel_exception is the root of the Lispel exception hierarchy. It is derived from the STL exception base class exception, so that you are able to catch Lispel and STL generated exceptions with one catch statement. Adding the core interpreter commands should always be wrapped into a try/catch block of its own as it doesn't make much sense to continue after a failure (unless you provide your own basic command set).

Adding user defined commands

Once you have created an interpreter instance you can export your applications functionality to the scripting level by registering commands with the interpreter. This section will show you how this is accomplished.

If you know how to export your applications functionality you should first select from the CommandImpl subclasses the one that best fits your needs. Normally you will use the BuiltinValue class which is the base class for implementing 'real' functions (i.e. not special forms). By implementing functions by defining some class you can use closures both on the C++ and the scripting language level.

Closures are functions augmented with some kind of state. In Scheme-like languages (like Lispel) this state is a snapshot of the (nested) bindings which were active when the closure was defined. In C++, you can add any kind of state to your function by simply adding instance variables to a function implementation. The following examples will demonstrate this by implementing the gensym function found in many Scheme implementations.

Implementing 'gensym'

Basic implementation

The gensym function in its first version is a very simple function in that it doesn't take any arguements. Each time called it will return a new (most probable) unique symbol. This kind of function can be used in functions generating functions, a quite customary thing in lisp-like languages. The symbols will be generated by using a counter which will be incremented each time the function is called. The following code fragment shows the declaration of the new command implementation:

#include <lispel/lispel.hh>

class GensymCommand : public BuiltinValue {
public:
   GensymCommand();
   virtual ~GensymCommand(); 
   virtual Handle_ptr execute( Context &ctx, Environment *env, vector<Handle_ptr> args);
protected:
   int m_counter;
};

The main work of the function will be handled in the execute method, which needs some explanation. The rest of the class declaration should be straight forward (Note the virtual declarations).

All values visible to Lispel are referenced using the type Handle_ptr, so this is the return of the main method. The value returned be execute will be used directly by the interpreter (so no copying takes place). The actual parameters of a function are also passed via this type. As our first implementation of gensym doesn't require any arguments, this can be ignored for now.

The first argument of execute is a structure which is used to hold references to the various components making up the currently active interpreter instance. It has been introduced to enable direct access to the interpreter's various parts. The alternative to this approach would have been to export every API details through the Interpreter class, which acts as a facade to the interpreter components. But this would definitely have bloated the Interpreter class. I prefer finding out what functionality is regularly needed before adding it to the high-level API. The very details of the interpreter's components will always have to be used by directly operating on the various components. The second argument to execute is a reference to the innermost binding environment active during a function application.

Now, let's move on to the command implementation. The following code implements the gensym function:

#include  //sprintf()

GensymCommand::GensymCommand()
{
   m_counter = 1;
}

GensymCommand::GensymCommand() {}

Handle_ptr GensymCommand::execute( Context &ctx, Environment *env,
                                   vector<Handle_ptr>)
{
   if (0 != args.size()) {
     cerr << "error: " << name() << " doesn't require any arguments" << endl;
     return ctx.NIL;
   }

   char buffer[64];
   sprintf( buffer, "gensym%d", m_counter++);

   return ctx.factory->makeSymbol( buffer);
}

The thing is to check the number of arguments passed to our function. If any arguments have been passed a message indicating what went wrong will be printed and the function will return with the empty list as a value. Later versions of Lispel will add proper error handling so that you don't have to print error messages to cerr. Until that, you can redirect cerr by assigning stream buffers to the cerr stream. Note that the method name() returns the name under which a function has been registered. The next part of the code is quite straight forward and shouldn't need any explanation except that it would be better to use string streams and add some more error checking. Finally, a new memory cell is allocated containing the new symbol.

The return statement might need some more explanation. All values visible on the Lispel language level are allocated on a heap. This heap is decorated by a factory class for the various Lispel types, called NodeFactory. This factory is the only way to instantiate valid Lispel values. To register the new function with an interpreter instance, call the method addBuiltin defined in Interpreter. The following code demonstrates this:

#include 
#include "gensym.hh"
...
Interpreter interp;
interp.addBuiltin( "gensym", new GensymCommand());

META: The following is an outline of examples based on gensym.

  1. (gensym) --> gensym: common prefix with counter
  2. (gensym ) --> : prefix can be given as an argument, counter is global to all prefixes
  3. (make-gensym >) --> , () --> : create a new gensym function which produces new symbols with one prefix and with one counter for each prefix
  4. Using the function adapter

Specifying symnbol prefixes

Returning costum closures

Configuring interpreter instantiation

Adding new types

⚠️ **GitHub.com Fallback** ⚠️