Mapping S-Expressions to Objects

While S-expressions can alleviate some of the hassles of parsing, you still have to get the S-expression into a form you can work with. You need to be able, for example, to turn an S-expression representing a function definition into some form that you can eventually use to create a JFuncMachine MethodDef.

The org.jfuncmachine.sexprlang.translate package provides a way of annotating classes so that it is possible to map an S-expression directly onto those classes. It can be a little difficult to understand at first.

Mapping by Initial Symbol

One way to map an S-expression to a Java object is by looking for a particular symbol as the first element in the S-expression. For example, suppose I am defining a very simple integer expression language and I want to define a function. I could make a definition like this:

public record FunctionDef(String name, String[] params, IntExpr body) {
}

In this case, the function just has a name, a list of parameter names, which are all assumed to be integers, and a body which is some subclass of IntExpr. I would like to be able to represent this as an S-expression this way:

(define add (x y) (+ x y))

The first step here is to annotate FunctionDef to say that it is defined by an S-expression that starts with define:

@ModelItem(symbol = "define")
public record FunctionDef(String name, String[] params, IntExpr body) {
}

SexprToModel is then able to see that it should try to turn an S-expression starting with the symbol define into a FunctionDef. It will look at all the public constructors of a class when trying to do the mapping, so if there is some variation in how the function is defined, you may be able to represent that with different constructors.

Enums

SexprToModel will have no problem mapping the (x y) to the array of strings named params in FunctionDef, but the body can be a bit trickier. In this case, where the body is (+ x y), we want it to map to an IntBinaryExpr class, and we can use a handy mechanism to support a binary expression with several different operators. This is how IntBinaryExpr is defined:

@ModelItem(includeStartSymbol = true)
public record IntBinaryExpr(ExprType exprType, IntExpr left,
  IntExpr right, String filename, int lineNumber)
  implements IntExpr {
    @ModelItem(isExprStart = true)
    public enum ExprType {
        Add("+"),
        Sub("-"),
        Mul("*"),
        Div("/");

        public final String symbol;
        ExprType(String symbol) {
            this.symbol = symbol;
        }
    }
}

So, first look at the nested ExprType enum. The enum is defined so that it takes a string symbol in its constructor, and these symbols are specified as part of the enum (e.g. “+” as the symbol for the Add enum). The isExprStart parameter in the @ModelItem annotation tells SexprToModel that the symbol associated with the enum can begin an S-expression. The mapper will assume that the expression that it can begin is the expression containing the enum (IntBinaryExpr in this case).

Now, moving up to the parent class, the includeStartSymbol parameter in the @ModelItem annotation tells the mapper to include the enum when looking for a matching constructor for the expression. Notice that the previous FunctionDef class did not include a parameter for the define keyword in its constructor. That’s because includeStartSymbol defaults to false. But here, we want to include it because the start symbol determines what kind of expression this is.

Constants

While you can have an int or Integer, double or Double, or String in a constructor and have the mapper map an SexprInt, SexprDouble, SexprString, or SexprSymbol to its corresponding values (it will try to map a symbol to a string if possible), you may also want to specify a class that represents a constant of one of these types of values. This is particular useful, for example, when you have expressions where the expression could just be an integer constant. In that case, there is no constructor you are trying to match the SexprInt with, so how does the mapper know what class to create?

You can create a class that represents a standalone int, double, string, or symbol with the isIntConstant, isDoubleConstant, isStringConstant or isSymbolExpr parameters to @ModelItem. For instance, here is how you can define an int constant expression:

@ModelItem(isIntConstant = true)
public record IntConstantExpr(int value, String filename, int lineNumber)
  implements IntExpr {
}

Varargs and Default Classes

One of the trickiest parts of mapping an S-expression to a Java object is the case where the first symbol is a function name. The mapper doesn’t know what names are value function names, so how does it know what to do?

What makes this extra difficult is that different functions may take a different number of parameters. Here is how you can define a function call in a simple integer expression language that can take a variable number of parameters:

@ModelItem(defaultForClass = IntExpr.class, includeStartSymbol = true,
  varargStart = 1)
public record FunctionCall(String name, IntExpr[] args,
  String filename, int lineNumber) implements IntExpr {
}

The defaultForClass parameter tells the mapper that if it is expecting to see some subclass of IntExpr and it can’t figure out which one, try this one. Thus if it sees a symbol it doesn’t recognize, it will try this class.

The includeStartSymbol again tells the mapper to include the initial symbol when looking for a constructor of FunctionCall that matches the current S-expression.

Finally, the varargStart parameter tells the mapper that the constructor takes a variable number of arguments (of the same type) starting at a certain position (1 here, where the first argument is numbered 0).