A Guide to LISPsmos

Source code here

About this guide

This is a guide to LISPsmos, the programming language which compiles to Desmos expressions. This document is CTRL+F-friendly, so if you're looking to do a specific thing, just hit Ctrl+F, and you'll be likely to find the result with few (if any) false positives!

What is LISPsmos?

I like to create horrendously overcomplicated things in Desmos— a 3D maze, a photorealistic 3D fractal that took ten hours to render, and a 3D game where you fly a plane, to name a few. However, working in Desmos is somewhat challenging... to say the least. So I wanted to create a tool to make it easier. And thus, I created LISPsmos.

LISPsmos is— put simply‐ a programming language that compiles to Desmos expressions. Well, it's kind of more than that: It's a text-based language you can use to build entire Desmos graphs from the ground up.

Installation / Quick Start

  1. Get NodeJS, if you haven't already.
  2. Create a directory in which you will put your first LISPsmos project. Call it whatever you want.
  3. Go to the terminal, navigate to the directory you've created, and type in npm install lispsmos. This will allow you to use LISPsmos
  4. Create a file index.mjs in the directory. Paste the following code into index.mjs:
    import * as lispsmos from "lispsmos";
    
    lispsmos.buildServer(async () => {
        let compiler = new lispsmos.compiler.LispsmosCompiler();
        console.log("Compiled!");
        return await compiler.compile("(displayMe (= y (^ x 3)))")
    })
        
    This code will create a web server which— when accessed— will compile some LISPsmos code into a Desmos Graph state object and send it to whoever requested it. We will use this web server to compile LISPsmos code and send it to Desmos where it can be displayed as a graph.
  5. Type node index.mjs to start the web server.
  6. Try testing your web server by going to http://localhost:8090. You can use a custom port and hostname (second and third parameter to buildServer) if you so desire.
  7. Now that we've created a LISPsmos server, we need to tell Desmos how to receive the LISPsmos graph. First, get Tampermonkey.
  8. Install this Tampermonkey userscript, which will add a text box to Desmos which will enable you to load Desmos graph state from any source.
  9. Once the userscript is installed and enabled, find the new textbox in Desmos in the bottom-right corner, type http://localhost:8090 into it, and press the "Grab State" button. You should see that the graph has changed to show the equation Y=x3. This userscript does the exact same thing that you did when you tried to go to http://localhost:8090— it requests the Desmos state, which the server promptly compiles. However, after that, it gives the graph state to Desmos, which displays it like it would any other graph.

If you managed to get everything in this section working, move onto the next section.

Installation, Continued

The web server we created in the last part was a minimal example— enough to get LISPsmos working, but a very suboptimal way of using it. You may have noticed that we are directly compiling a string literal. As you can imagine, this will be inconvenient to edit, should we want to add more LISPsmos code. Let's make it get the LISPsmos code from a file called main.lisp instead. Replace the contents of index.mjs with the following:

import * as lispsmos from "lispsmos";
import * as fs from "node:fs";

lispsmos.buildServer(async () => {
    let compiler = new lispsmos.compiler.LispsmosCompiler();
    lispsmos.utilityMacros.register(compiler);
    console.log("Compiled!");
    return await compiler.compile(fs.readFileSync("main.lisp"))
})
                    

Now, create a file main.lisp in the same directory and put the following in it:

(displayMe (= y (^ x 3)))

If you try running the web server again, you should end up displaying the same equation as before. However, this time, LISPsmos will be compiling the code in the file main.lisp, not the string literal.

Syntax

Polish Notation

LISPsmos follows an extremely simple (but quite unorthodox) syntax called Polish notation. Normally, when you're doing math— for instance, 1 + 2 + 3— you put the operator + between the operands 1, 2, and 3. In LISPsmos, which uses Polish notation, you would write the same expression like this: (+ 1 2 3). The first thing in the list— the "+," is treated as an operator, whereas everything after it is treated as its operands. This may seem very odd to some. However, there are some advantages to this approach which I hope you will come to appreciate: For one, it's extremely consistent— every operator in LISPsmos works like this, making one's mental model of the syntax much simpler. For another advantage, it also cuts down on extra operators. Notice how we only used a single + by using Polish notation. Conversely, with the more "standard" infix notation, we used two of them! On top of all this, Polish notation isn't actually that far off from what we already do with other functions: For instance, sin(x) becomes (sin x) and mod(x, 2) becomes (mod x 2).

Do be aware, however, that the semantics of LISPsmos are somewhat more complicated than this at times— some complex operators may define slightly different rules that do not exist outside of them (which will be explained as they come up). However, the underlying syntax will remain the same.

Basic Examples

Here are some examples of LISPsmos syntax compared to (approximately) how they would appear in Desmos:

LISPsmosDesmos Output (approx)
(+ 1 2 3 4 5) \left(1+2+3+4+5\right)
(* 5 (+ 2 3)) \left(5*\left(2+3\right)\right)
(sin (* 3 x)) \sin\left(\left(3*x\right)\right)

Variables

Create a variable with the = operator. Be aware that (like everything in LISPsmos) this follows Polish notation. Here is an example

LISPsmosDesmos Output (approx)
(= a 5) a=5
(= y (^ x 2)) y=x^{2}

Variables can have multi-character names. LISPsmos will represent these with subscripts in Desmos.

LISPsmosDesmos Output (approx)
(= foo 12) f_{oo}=12

Functions

Create a function with the fn keyword, followed by its name, then its arguments separated by spaces, and then finally its actual definition in a single block. Take note of the second example— LISPsmos does not care about line breaks in the middle of expressions. Use them to make long expressions easier to read.

LISPsmosDesmos Output (approx)
(fn square x (^ x 2))
(square 4)
s_{quare}\left(x\right)=x^{2}
s_{quare}\left(4\right)
(fn dist x1 y1 x2 y2 (sqrt (+ 
        (^ (- x1 x2) 2)
        (^ (- y1 y2) 2)
    )))
d_{ist}\left(x_{1},y_{1},x_{2},y_{2}\right)=\sqrt{\left(\left(x_{1}-x_{2}\right)^{2}+\left(y_{1}-y_{2}\right)^{2}\right)}

Display/Show Expressions

Display an Expression

Up until now, you won't have seen anything on the screen upon loading your LISPsmos expressions. This changes now— use the displayMe operator to display an expression. The expression should be the first argument to displayMe. Be warned that you can only put a single expression into displayMe.

LISPsmosDesmos Output (approx)
(displayMe (= y x)) y=x

Display Properties

You can set "display properties" of an expression to customize the way it is displayed. For instance, the graph produced by the following LISPsmos code (not shown on screen) will have a line width of 20 pixels, be dotted, have 20% opacity, and be a bright red color.

LISPsmosDesmos Output (approx)
(displayMe (= y x)
        (lineWidth 20)
        (lineStyle DOTTED)
        (lineOpacity 0.2)
        (color RED)
    )
y=x

Points

LISPsmosDesmos Output (approx)Description
(point 0 0) \left(0,0\right) Creates a point at the origin, (0, 0).
(= pt (point 3 4))
(.x pt)
p_{t}=\left(3,4\right)
p_{t}.x
Creates a point called pt, and then finds its x-coordinate with the ".x" operator. The second expression should evaluate to 3.

Lists

Create a list with the builtin list function. These lists can also contain ranges with ...

LISPsmosDesmos Output (approx)
(list 1 2 3 4 5 6 7) \left[1,2,3,4,5,6,7\right]
(list 1 ... 20) \left[1,...,20\right]

Piecewises

Create a piecewise with the builtin piecewise function. Every argument to the piecewise function takes the form of a parenthesized pair of statements. The first one is the condition for that branch of the piecewise, whereas the second is the value which the piecewise is to evaluate to.

LISPsmosDesmos Output (approx)Description
(piecewise ((> x 0) x) ((<= x 0) (* -1 x))) \left\{x>0:x,x<=0:\left(-1*x\right)\right\} The absolute value function, represented as a piecewise.
(piecewise ((> x 0) x) ((* -1 x))) \left\{x>0:x,\left(-1*x\right)\right\} The condition for the last branch of the piecewise can be omitted to mean that it should be used when none of the other branches match.

Actions

Just as a variable can be created with =, an action can be created with ->.

LISPsmosDesmos Output (approx)Description
(= a 0)
(-> a (+ a 1))
a=0
a\to\left(a+1\right)

Viewport/Graph Bounds

Want to change the size of the default starting graph? LISPsmos can do that too. For instance, the line (viewport -2 2 -2 2) will try to make the graph range from -2 to 2 on the x-axis, and -2 to 2 on the y-axis. However, Desmos will still try to preserve aspect ratio, so this will not be exact!

Folders

Create a folder with the folder function. The following defines a folder named "equations":

(folder ((title "equations"))
    (= y x)
    (= y (^ x 2))
    (= y (^ x 3))                   
)

Macros

Macros are one of LISPsmos's most powerful features. They allow you to eliminate repetitive sections of code which would otherwise be tedious to write by hand. Macros are not enabled by default in LISPsmos— you can enable them by adding the following line to your JS file immediately after defining the variable compiler:

lispsmos.utilityMacros.register(compiler);
LISPsmosDesmos Output (approx)Description
(defineFindAndReplace plusOne variable ((+ variable 1)))
(plusOne x)
\left(x+1\right) The first line of LISPsmos code defines a "find-and-replace" macro called plusOne. This macro takes a single argument called variable and returns a statement which adds one to that variable. The second line of code calls that macro with x as a parameter. You can imagine an intermediate step here where (plusOne x) is substituted with (+ variable 1) (as per the macro definition)— except variable is x (since we're calling plusOne with x), so we end up with (+ x 1)
(inlineJS "return [['=', 'y', ['*', 'x', '3']]]") y=\left(x*3\right) inlineJS macros are an extremely powerful feature of LISPsmos that let you define snippets of JavaScript code that create parsed LISPsmos code at compile time. Parsed LISPsmos code becomes what I call "AST Nodes." An AST Node can be one of two things: a string, or an array of AST Nodes. An AST node represents a list of items enclosed within parentheses, whereas a string represents an individual item— a variable, operator, function name, macro name, et cetera.