lisp utilities page

SAVA

Abstract

Sava is a java sexp utility for common lisp. It is useful when you are obliged to use Java. It allows you to extend java syntax. It enables you to do meta-programming.

Features

- parsing of sava sexp to a sava object tree.
- pretty printing of sava trees using java syntax..
- useable at the REPL.
- extensible.
- embedded language.
- free.

Contents

- Installing Sava
- A full example : divelog
- Extending java syntax by extending sava
- Extending parsing
- Extending ouput
- Conclusion
- Versions
- Thanks

Installing SAVA

You may test it with Clisp, but since i use pretty-printing which seems buggy in Clisp, the output won't be guaranteed !. Instead you may use the XP hack i made which works with CLisp 2.33.

It uses split-sequence.

After downloading and extracting this file, change the path defined at the start of each file. Then type the following lines in your lisp. (i will soon look at asdf-install ;-) )

(defun init ()
#+clisp
  (progn
    (load "o:/lisp/xp/xp-code.lisp")
    (eval `(,(find-symbol (string :install) :xp))) )
  (load "p:/lisp/utils/split-sequence.lisp")
  (load "p:/lisp/utils/utils.lisp")
  (use-package "UTILS" "USER") )

(defun init-sava ()
  (eval '(setf *default-pathname-defaults* #P"p:/lisp/sava/"))
  (load (merge-pathnames "sava-user.lisp"))
  (eval '(progn
            (setf (readtable-case *readtable*) :invert)
            (setf *print-case* :downcase)
	    (setf *package* (find-package '#:sava-user)) ))
  (load (merge-pathnames "divelog-sava.lisp")) )

> (init)

> (init-sava)

A full example : divelog

We will see how SAVA generates this "Divelog.java" file from its sava form. Divelog.java comes from Divelog blue print application found somewhere on sun site.

package divelog;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*; 


public class DiveLog 
{ //Opens DiveLog class
 
  private JTabbedPane tabbedPane;
  private JFrame dlframe; 
      
      public DiveLog()
      { //Opens DiveLog constructor
      
      //Create a frame object to add the application
      //GUI components to.
      
        dlframe = new JFrame("A Java(TM) Technology Dive Log");
        
        // Closes from title bar
        //and from menu
        dlframe.addWindowListener(new WindowAdapter() 
        { // Opens addWindowListener method
          public void windowClosing(WindowEvent e)
          { // Opens windowClosing method
             System.exit(0);
           } // Closes windowClosing method
         }); // Closes addWindowListener method
         
                  
         // Tabbed pane with panels for Jcomponents
         
         tabbedPane = new JTabbedPane(SwingConstants.LEFT);
         tabbedPane.setBackground(Color.blue);
         tabbedPane.setForeground(Color.white);
         
         // Calls a method that adds individual tabs to the
         //tabbedpane object.
         populateTabbedPane();
         
         //Calls the method that builds the menu 
         buildMenu();
         
         dlframe.getContentPane().add(tabbedPane);
         
         dlframe.pack();
         dlframe.setSize(765, 690);
         dlframe.setBackground(Color.white);
         dlframe.setVisible(true);
         } // Ends class constructor
         
         private void populateTabbedPane()
           { // Opens populateTabbedPane method definition
           // Create tabs with titles
           
           
           tabbedPane.addTab("Welcome",
                               null,
                               new Welcome(),
                               "Welcome to the Dive Log");
                        
                               
           tabbedPane.addTab("Diver Data",
                               null,
                               new Diver(),
                               "Click here to enter diver data");
                               
           tabbedPane.addTab("Log Dives",
                               null,
                               new Dives(),
                               "Click here to enter dives");
                               
           tabbedPane.addTab("Statistics",
                               null,
                               new Statistics(),
                               "Click here to calculate dive statistics");
                               
           tabbedPane.addTab("Favorite Web Site",
                               null,
                               new WebSite(),
                               "Click here to see a web site");                
           tabbedPane.addTab("Resources",
                               null,
                               new Resources(),
                               "Click here to see a list of resources");         
                     } //Ends populateTabbedPane method
          
         
         private void buildMenu()
         { // Opens buildMenu method definition
           JMenuBar mb = new JMenuBar();
           JMenu menu = new JMenu("File");
           JMenuItem item = new JMenuItem("Exit");
           
           //Closes the application from the Exit 
           //menu item.
           item.addActionListener(new ActionListener()
           { // Opens addActionListener method
             public void actionPerformed(ActionEvent e)
             { // Opens actionPerformed method
                System.exit(0);
              } // Closes actionPerformed method
              
             }); // Closes addActionListener method
             
             menu.add(item);
             mb.add(menu);
             dlframe.setJMenuBar(mb);
             }// Closes buildMenu method
          

     // main method and entry point for app    
     public static void main(String[] args)
     { // Opens main method
     
      DiveLog dl = new DiveLog();
      
      } // Closes main method
                     
 } //Ends class DiveLog                   

*cu-divelog* (in "divelog-sava.lisp") contains the sava translation of this file. You don't have to type it, you already have it in your *cu-divelog*. The '$' is for changing the package you are reading in for the next sexp. Below the sexpr '(cu ...' is read from the :sava-parser package. You may use it for other applications.

(setq *cu-divelog*
$ava-parser
(cu
   (package divelog)

   (import javax swing *)
   (import java  awt   *)
   (import java  awt   event *)

   (class public DiveLog
	  (field private JTabbedPane tabbedPane)
	  (field private JFrame dlframe)

	  (constructor public ()
		       (= dlframe (new JFrame "A Java(TM) Technology Dive Log"))
		       (call dlframe addWindowListener (new WindowAdapter :anonymous
							    (method public void windowClosing (WindowEvent e)
								    (call System exit 0) )))
		       (= tabbedPane (new JTabbedPane SwingConstants.LEFT))
		       (call tabbedPane setBackground (get Color blue))
		       (call tabbedPane setForeground (get Color white))
		       (self-call populateTabbedPane)
		       (self-call buildMenu)
		       (call (call dlframe getContentPane) add tabbedPane)
		       (call dlframe pack)
		       (call dlframe setSize 765 690)
		       (call dlframe setBackground (get Color white))
		       (call dlframe setVisible true) )

	  (method private void populateTabbedPane ()
		  (call tabbedPane addTab "Welcome" null (new Welcome) "Welcome to the Dive Log")
		  (call tabbedPane addTab "Diver Data" null (new Diver) "Click here to enter diver data")
		  (call tabbedPane addTab "Log Dives"  null (new Dives) "Click here to enter dives")
		  (call tabbedPane addTab "Statistics" null (new Statistics) "Click here to calculate dive statistics")
		  (call tabbedPane addTab "Favorite Web Site" null (new WebSite) "Click here to see a web site")
		  (call tabbedPane addTab "Resources" null (new Resources) "Click here to see a list of resources") )
         
	  (method private void buildMenu ()
		  (var JMenuBar mb (new JMenuBar))
		  (var JMenu menu  (new JMenu "File"))
		  (var JMenuItem item (new JMenuItem "Exit"))

		  (call item addActionListener (new ActionListener :anonymous
						    (method public void actionPerformed (ActionEvent e)
							    (call System exit 0) )))
		  (call menu add item)
		  (call mb   add menu)
		  (call dlframe setJMenuBar mb) )

	  (method public static void main ((array String) args)
		  (var DiveLog dl (new DiveLog)) ))))

Sava is an embedded language. This means that it is translated in lisp code by macros. This way your application can be compiled by your lisp compiler.

Now, we just have to call the 'java' function to translate (from the REPL !) this sava object tree in java syntax. It uses a pretty printer dispatch table (file "sava-print.lisp"). So you may easily change the ouput. You may even write your own output program, using method on sava class for example.

? (java *cu-divelog*)

package divelog ;
import javax.swing.* ;
import java.awt.* ;
import java.awt.event.* ;
public class DiveLog
{
  private JTabbedPane tabbedPane ;
  private JFrame dlframe ;

  public DiveLog ()
  {
    dlframe = new JFrame( "A Java(TM) Technology Dive Log" ) ;
    dlframe.addWindowListener( new WindowAdapter()
                                   {
                                     public void windowClosing ( WindowEvent e )
                                     {
                                       System.exit( 0 ) ;
                                     } ;
                                   } ) ;
    tabbedPane = new JTabbedPane( SwingConstants.LEFT ) ;
    tabbedPane.setBackground( Color.blue ) ;
    tabbedPane.setForeground( Color.white ) ;
    populateTabbedPane() ;
    buildMenu() ;
    dlframe.getContentPane().add( tabbedPane ) ;
    dlframe.pack() ;
    dlframe.setSize( 765, 690 ) ;
    dlframe.setBackground( Color.white ) ;
    dlframe.setVisible( true ) ;
  }

  private void populateTabbedPane ()
  {
    tabbedPane.addTab( "Welcome", null, new Welcome(), "Welcome to the Dive Log" ) ;
    tabbedPane.addTab( "Diver Data", null, new Diver(), "Click here to enter diver data" ) ;
    tabbedPane.addTab( "Log Dives", null, new Dives(), "Click here to enter dives" ) ;
    tabbedPane.addTab( "Statistics", null, new Statistics(), "Click here to calculate dive statistics" ) ;
    tabbedPane.addTab( "Favorite Web Site", null, new WebSite(), "Click here to see a web site" ) ;
    tabbedPane.addTab( "Resources", null, new Resources(), "Click here to see a list of resources" ) ;
  }

  private void buildMenu ()
  {
    JMenuBar mb = new JMenuBar() ;
    JMenu menu = new JMenu( "File" ) ;
    JMenuItem item = new JMenuItem( "Exit" ) ;
    item.addActionListener( new ActionListener()
                                {
                                  public void actionPerformed ( ActionEvent e )
                                  {
                                    System.exit( 0 ) ;
                                  } ;
                                } ) ;
    menu.add( item ) ;
    mb.add( menu ) ;
    dlframe.setJMenuBar( mb ) ;
  }

  public static void main ( String[] args )
  {
    DiveLog dl = new DiveLog() ;
  }
}

You may a dump a compilation unit with the 'sava-user:gen.java' utility.

SAVA-USER> (gen-.java *cu-divelog* #p"p:/lisp/sava/")

Extending java syntax by extending sava

One of the problem with java is that you can't extend its syntax. In lisp you may do this so you can create Domain Specific Language for your applications. We will see that extending sava is very easy since you just have to define new functions or macros.

I will show you for example how to extend sava to import from a frequently used package hierarchy.

SAVA-USER[6]> (java $p(import x y z maClass)) ; sp is a nickname of sava-parser
import x.y.z.maClass ;

SAVA-USER> (defmacro sp::import-with-default-package (&rest args) `(sp::import x y ,@args))
sava-parser::import-with-default-package

SAVA-USER> (java $p(import-with-default-package z myClass))
import x.y.z.myClass ;

I find that it's not great to define such a new macro. After all, it has the same meaning of 'import'. What have change is only its arguments. So here is an other solution :

SAVA-USER> (defun sp::default-package () (values 'x 'y))
sava-parser::default-package

SAVA-USER> (java $p(import (default-package) z maClass))
import x.y.z.maClass ;

Note the use of values. When you want to splice your function's result, just use values ! To know how 'import' (or other) evaluates its argument look at the "Extending parsing" section.

A more useful example : import*. It allows you to import different classes from the same package directory.

SAVA-USER> (defmacro sp::import* (&rest args)
                 (let ((package-dir (butlast args))
                       (classes     (first (last args))) )
                  `(values ,@(loop for c in classes
                                   collect `(sp::import ,@package-dir ,c) ))))
sava-parser::import*

SAVA-USER> (java $p(cu (import* x y (maClass1 maClass2 maClass3))))

import x.y.maClass1 ;
import x.y.maClass2 ;
import x.y.maClass3 ;

As you see, you just have to define new functions and new macros to extend Sava. The goal was to make extensions as predictable as possible.

Extending parsing

Parsing is done by defining two things for each java syntactic element : a class, and a parser.

Sava classes are found in "sava.lisp" file. The class is a CLOS class defined by the macro def-class. Below the class definition for a java method. They are defined in the sava package.

(def-class method :fields  (modifiers type name arg-defs statements))

Sava readers are found in "sava-parser.lisp". A parser is like a macro. But instead of lambda-expression you will find a pattern to match a list. This uses utils:list-partition macro. You may use it for an other application. Parsers are defined in the sava-parser (sp) package. You may add your own parsers (see there). You may even not used these parsers at all.

(def-custom-parser method ((modifiers :0-* sava:modifier) type name (arg-defs 1 list) (statements :0-*))
  `(make-instance 'sava:method  :modifiers   ',modifiers
                                :type        ,(expr type)
                                :name        ,(expr name)
                                :arg-defs     (list ,@(loop for (type var) on arg-defs by #'cl:cddr
							    collect `(arg-def ,type ,var) ))
                                :statements   (multiple-value-call #'list ,@statements) ))

The 'method' parser match a list with the following pattern : modifiers is bound to zero or more x which are of type sava:modifier, type and name are then bound to the next two elements, arg-defs is bound to the next element which must be of type list, finally 'statements' is bound to the remaining elements.

The macro 'method' will return a 'sava:method' object with special evaluation rules for each field. 'modifiers' is just quoted. 'type' is quoted if it is a symbol, evaluated if it is a list, this is done by the 'expr' function. 'arg-defs' will list 2-by-2 'arg-def' objects. 'statements' will be evaluated and all values gathered to allow splicing by returning multiple values.

All sava classes and parsers are defined this way. And the example above is one of the most difficult. I hope it will help extending sava definitions. I even believe that it can be applied to lots of other sexp translations. It is near a language to define s-expr => object-tree.

When parsers are simpler, you can use 'def-parser' instead. I'm currently converting most 'def-custom-parser' forms into 'def-parser' ones.

(def-parser field sava:field ((modifiers :0-* sava:modifier) type name (value :0-1)))

is equivalent to :

(def-custom-parser field ((modifiers :0-* sava:modifier) type name (value :0-1))
  `(make-instance 'sava:field :modifiers ',modifiers
                              :type  ,(expr value)
                              :name  ,(expr name)
                              :value ,(expr value) ))

In Sava, all java keywords will hopefully be defined. If some are missing because it has been forgotten or a new java is released, you may extend Sava on your own. You may also change, add or remove parsers to suit your needs.

Extending output

It is as simple (or as complex ;-)) as using the pretty printer facility. in the "sava-printer.lisp" file, there's a pprint-dispatch for each sava class.

(defmacro def-printer (type &body body)
  `(set-pprint-dispatch ',type
    (lambda (s x)
      ,@body ) ))

(def-printer sava:class
    (let ((*class-name* (sava:name x)))
      (format s "~{~W ~}class ~W" (sava:modifiers x) (sava:name x))
      (pprint-newline :mandatory s)
      (pprint-logical-block (s (sava:members x))
	(write-char #\{ s)
	(pprint-indent :block 2 s)
	(dolist (field (remove-if-not (lambda (x) (typep x 'sava:field)) (sava:members x)))
	  (pprint-newline :mandatory s)
	  (write field :stream s) )
	(awhen (remove-if-not (lambda (x) (typep x '(or sava:method sava:constructor))) (sava:members x))
	       (dolist (method it)
		 (pprint-newline :mandatory s)
		 (pprint-newline :mandatory s)
		 (write method :stream s) )))
      (pprint-newline :mandatory s)
      (write-char #\} s) ))

The pretty printing is actually basic. But it can easily be enhanced. You may even use your own printing utility for sava objects.

Conclusion

Sava needs more work : a better packaging (asdf, common-lisp.net), handling of all java syntax, extensions(J2ME, ...) and better pretty-printing. And to do this, i have now to use it extensively.

Versions

v0.4, 2004-10-24 : 'def-parser', 'def-custom-parser', 'def-printer'. :invert/:downcase for case-sensitivity handling.
         2004-10-17 : hacking R.C. Waters's XP to make it works with Clisp 2.33.
v0.3, 2004-10-08 : 'def-reader' replaced by 'def-custom-reader' and new 'def-reader' added.
v0.2, 2004-10-01 : structures replaced by classes. 'def-reader' added to allow more extensible s-expr->sava-tree parsing.
v0.1, 2004-09-25 : parsing & printing.

Thanks to

Thanks to c.l.l. people. Special thanks to Pascal Bourguignon, Antonio Menezes Leitao and Edward Marco Baringer.

Contact : Christophe Turle, (mailto :name (c turle) :address (wanadoo fr))

lisp utilities page