Lisp Basics: Primer for Beginners
If you’re already familiar with Lisp, feel free to skip this section and proceed to the next.
Lisp source code consists of s-expressions, which are essentially lists. In an s-expression, the first element represents a function, while the remaining elements are the function’s arguments.
In this case, (greet “honza”) is a list with two items. “greet” is the function name, and “honza” is the argument. In other programming languages, this might be represented as greet(“honza”).
Lisp employs s-expressions for various purposes, including function definitions, if statements, assignments, binary expressions, and more.
Here are a few examples of Lisp code snippets:
Defining a variable called “name” and assigning it the value “honza”:
Adding 1 and 2 and returning the result:
Conditional statement that prints “hey honza” if the variable “name” is equal to “honza,” otherwise prints “hey stranger”:
Function definition for a function called “greet” that takes one parameter called “name”:
In Lisp, a function body can have multiple s-expressions, but only the last one is returned. Unlike other languages, Lisp doesn’t require a return keyword. Binary operators and keywords like “if” are actually functions that return values.
Unleashing the Mighty PEG Grammar
Every PEG grammar begins with the program directive, which serves as the starting point for parsing.
To successfully compile this grammar into a parser, we need to define the structure of an s-expression.
An s-expression can be an atom, a list, a vector, or an object. Each of these can be preceded and followed by any amount of whitespace.
Let’s examine the atom:
An atom can be a sequence of digits, a string enclosed in double quotes, or a valid identifier. When a digit sequence is matched, it is assigned to the d variable. We concatenate the digits and convert them into an integer using the numberify function. Both numbers and strings are considered literal values and are returned as such. Identifiers, which represent variable names, are also returned as an identifier object.
Moving on to vectors and objects:
Similarly, a vector can be an empty array, an array with one or more atoms, or an array with one or more objects. The makeObject function takes pairs of elements from the array and converts the first item into an object key, setting its value as the second item. If the array length is not divisible by 2, an error is triggered.
Lastly, let’s explore lists. Lists are special because the first item represents the name of a function.
A list can either be an empty list or a list of one or more s-expressions. When dealing with a non-empty list, we check the first element to determine its type. If it’s “def,” we handle it as a variable declaration. If it’s “fn,” it represents an anonymous function. If it matches a built-in function or a user-defined function, we process it accordingly. The processCallExpression function handles function calls, distinguishing between statements and expressions.
To complete the grammar, we define whitespace:
Whitespace consists of zero or more newline, comma, or space characters.
Statement vs. Expression
This function checks if any arguments passed to a function call are also function calls. If a nested function call is detected, it is represented as a CallExpression. Otherwise, it is treated as a CallExpression within an ExpressionStatement. Since the PEG parser lacks context awareness, this distinction must be made explicitly.
In Lisp, the last s-expression in a function’s body is implicitly returned. There’s no need to indicate this with a return statement; it’s built into the language. For function declarations, additional processing is required to identify the last expression and wrap it in a ReturnStatement.
If Statement as an Expression
In Lisp, the “if” statement is an expression, just like a function call. This means that the expression in either of the two branches is effectively returned to the caller. To handle this, extra wrapping is added around the statement, and each branch expression is enclosed in a return statement.
To make the Lisp experience even more enjoyable, a standard library was developed. Stored in a file called lib.js, it contains functions that can be accessed from any Lisp program you write. This enables the inclusion of fun functional programming functions and enhances the language’s capabilities.
Putting It All Together
Here’s an overview of the process:
- Use peg.js to compile the grammar into a parser.
- Combine the parser with the compiler program.
- To use the compiler, execute the following command: