LITE is an extensible text editor with a built-in, dynamic programming language to alter the editor’s functionality: LITE LISP.
LITE may mean anything that abbreviates the letters; listed here are a few that are especially significant.
- LISP INTERPRETER and TEXT EDITOR
- LITE IS TOTALLY EMACS
- LISP IMBUED with TONS of ECCENTRICITIES
The goal of LITE is to create a modern, cross-platform tool for text editing and general scripting that is easy to port.
Initially, development followed this LISP interpreter tutorial (archive).
One glaring difference between Emacs and LITE is that LITE does not use a Gap Buffer data structure to edit text; rather, it uses a Rope.
NOTE: Any and all shell commands assume a working directory of the base of this repository.
If you have the LITE executable, run it with the --help
argument for
usage information.
By default, LITE is compiled with a GFX backend, resulting in an executable GUI application.
Run it the way that you would normally run a graphical application on your OS. Most of the time this means double-clicking the executable from a file explorer, or launching the executable from a shell.
NOTE: For ongoing development, it is best to run builds of LITE with a working directory of this repository, so it can find the most up-to-date LISP sources, fonts, etc.
The executable may be obtained by building from local source.
LITE may be built as a terminal-only “read, evaluate, print, loop” program (a REPL).
To enter the REPL, run the following:
./bin/LITE
On Windows:
.\bin\LITE
Any and all arguments following a --
argument are treated as file
paths and are attempted to be loaded as LITE LISP source files.
CMake is used as the cross-platform build system.
To see a list of available build systems, use the following command:
cmake -G
I recommend Ninja, it is modern, fast, and designed for use in conjunction with CMake.
First, generate an out-of-source build tree:
cmake -G <build system> -B bld -DCMAKE_BUILD_TYPE=Release
Replace <build system>
with one of the available build systems
Optionally, build LITE with graphical user interface capabilities by
adding the flag -DLITE_GFX=ON
when generating the build tree.
Ensure to read the README in the ~gfx~ subdirectory, if doing so.
Once the build tree is generated, invoke it to generate an executable.
cmake --build bld
If no fatal errors occur, the bin
subdirectory of the
repository will be populated with the LITE executable.
In order for LITE to find the standard library at runtime, it is
necessary to install LITE. This creates a directory in which LITE will
look during runtime for certain resources like fonts or lisp. On Linux,
this is located at $HOME/lite
. On Windows, it’s at %APPDATA%/lite
.
cmake --install bld
LITE LISP is the language that LITE interprets.
Every LISP program is made up of atoms. An atom is nearly any sequence of bytes, except for whitespace, commas, or backslashes. Atoms are NOT case sensitive.
Here are some valid LITE LISP atoms.
foo-bar foo/bar *foobar* <*-/im-an-atom/-*> -420 69 interrupt80
A list is a sequence of atoms and/or other lists surrounded by parentheses.
(foo-bar foo/bar *foobar*) ( im a list (in a list )) (sun mon tue wed thu fri sat)
Strings are any sequence of bytes surrounded by double quotes.
"I am a string." " al;sdn (!*^(*%)#!^) \033 \n lasnk \ a \a \t \f " "Information is easy to fake but hard to smell."
Comments begin with a semi-colon and stop at the first newline.
; I'm a comment ;;;; And I as well! (print "hello, friends!") ; print to stdout
Function calls are represented as a list with a symbol as the first element, and any arguments passed are subsequent elements.
(print "hello friends!") (abs -69420) (define foo 42)
The first element in a list that is to be evaluated is referred to as
the operator
.
Every object in LISP is called an Atom
. Every Atom has a type, a value,
a docstring, and a generic allocation pointer associated with it.
The value is a union with multiple value types, and the type field designates which value within the union to use, and how to treat it.
The docstring is a string containing information about the atom, i.e. documenting it.
This could range from a function’s usage to a variables meaning. \
Access docstrings using the docstring special form: (docstring <atom>)
.
The generic allocation pointer is a linked list of allocated memory that may be freed when the atom is garbage collected. This allows the LITE interpreter to allocate memory as needed and ensure it is freed /after/ using it.
Here are the different types an Atom may have in LITE LISP:
- Nil
- This is the definition of false, nothing, etc.
- Pair
- A recursive pair, containing a left-hand Atom and a right-hand Atom.
A pair has special terminology for the two sides; the left is referred to as
car
, while the right is referred to ascdr
.A list is a pair with a value on the left, and another pair, or nil, on the right.
- Symbol
- A sequence of bytes that may be bound in the environment.
All symbols are located in the symbol table with no duplicates.
- String
- A sequence of bytes, usually denoting human readable text.
- Integer
- An integer number, like
1
,-420
, or69
. - BuiltIn
- A function implemented in LITE source code that is able to be called from LITE LISP.
- Closure
- A function implemented in LITE LISP; a lambda.
- Macro
- A closure with unevaluated arguments that creates an expression that is then evaluated.
- Buffer
- An opened file that may be edited in LITE.
Variables are stored in an environment. An environment is a key/value dictionary, where the keys are a symbol, and the values are atomic LISP objects. When evaluating a symbol, it is first checked if there is a binding in any accessible environment. If so, that value is used in place of the symbol, when evaluated.
To bind a symbol to a value in the local environment, use the DEFINE
special form.
(define new-variable 42)
NOTE: DEFINE
will first attempt to find the symbol in any parent environment; if found, it will override that binding’s value instead of creating a new one in the immediate environment. This allows for DEFINE
to set the value of parameters, LET
arguments, etc.
To bind a symbol to a value in the global environment, use the SET
special form.
(set new-variable 42)
new-variable
is now a symbol bound in the environment. Following occurences of the bound symbol will be evaluated to the defined value, 42
.
Sometimes, it is useful to not evaluate a variable. This can be done using the QUOTE
operator.
(quote new-variable) ; returns the symbol "new-variable"
As quoting is a very common necessity in LISP, there is a special short-hand for it: a preceding single-quote. This short-hand means the following to be equivalent to the QUOTE
just above.
'new-variable ; returns the symbol "new-variable"
When defining any variable, it is possible to define a docstring for it by specifying it as a third argument:
(define new-variable 42 "The meaning of life, the universe, and everything.")
The docstring may be accessed with a builtin, like so:
(docstring new-variable)
The standard library includes a macro to help re-define a docstring:
(set-docstring new-variable "The meaning of your mom.")
This allows for everything in LITE LISP to self-document it’s use.
The standard library includes the DEFUN
macro to help define named functions.
(defun NAME ARGUMENT DOCSTRING BODY-EXPRESSION(S))
Here is a simple factorial implementation that works for small, positive numbers:
(defun fact (x) "Get the factorial of integer X." (if (= x 0) 1 (* x (fact (- x 1)))))
To call a named function, put the name of the function in the operator position, and any arguments following. Arguments are evaluated before being bound and the body being executed.
(fact 6)
Assuming FACT
refers to the function defined just above, this would
result in the integer 720
, as 6
was bound to the symbol X
during
the execution of the functions body.
As arguments are evaluated before being bound, we can also pass expressions. The result of the expression will be bound to the argument symbol.
(fact (fact 3))
In this case, (fact 3)
will be evaluated before the outer FACT
call, so that we can bind the result of it to X
. Once evaluating,
we will get the integer result 6
, which will then be bound to X
in the outer (left-most) FACT
call, resulting in 720
.
A lambda is a function with no name.
Currently, lambdas may be defined with the following special form:
(lambda ARGUMENT BODY-EXPRESSION(S))
ARGUMENT is a symbol or a list of symbols denoting arguments to be bound when the function is called.
BODY-EXPRESSION(S) is a sequence of expressions that will be executed with arguments bound when the lambda is called. The result of the last expression in the body is the return value of the lambda.
This means the identity lambda may be written like so:
(lambda (x) x)
As a real world example, here is the factorial implementation from above written as a lambda:
(lambda (x) (if (= x 0) 1 (* x (fact (- x 1)))))
To call a lambda, put it in the operator position just like the name of a named function. Pass any arguments as subsequent values in the list, just as you would a named function.
((lambda (x) (if (= x 0) 1 (* x (fact - x 1)))) 6)
Evaluating the above would result in the integer value 720
, as 6
was bound to X
and the lambda body was executed.
There is also support for variadic arguments using an improper list. The syntax for an improper list is as follows:
(1 2 3 . 4)
In the context of a lambda, here is how to define a function with two positional arguments followed by a varying number of arguments.
(lambda (argument1 argument2 . the-rest) BODY-EXPRESSION(S))
After all fixed arguments are given, the rest are passed as a list to the function. If no variadic arguments are given, nil is passed.
To create a function that may take any amount of arguments, put a
symbol in the ARGUMENT position, as seen in this re-definition of the
+
operator in the standard library:
(let ((old+ +))
(lambda ints (foldl old+ 0 ints)))
A macro may be created with the MACRO
operator.
A macro is like a lambda, except it will return the result of evaluating
it’s return value, rather than it’s return value being the result.
This allows for commands and arguments to be built programatically in LISP.
In order to ease the making of macros, there is quasiquotation. It is similar to regular quotation, but it is possible to unquote specific atoms so as to evaluate them before calling the returned expression.
While it is possible to call the quasiquotation operators manually, there are short-hand special forms built in to the parser.
- ’`’ – QUASIQUOTE
- ’,’ – UNQUOTE
- ’,@’ – UNQUOTE-SPLICING
These special forms allow macro definitions to look more like the expressions they produce.
A simple example that mimics the QUOTE
operator:
(macro my-quote (x) "Mimics the 'QUOTE' operator." `(quote ,x))
The QUASIQUOTE special-form at the beginning will cause the QUOTE
symbol to pass through without being evaluated. The UNQUOTE
special-form before the X
symbol will cause it to be evaluated,
replacing ,x
with the passed argument.
For example, calling (my-quote a)
will eventually expand to
(QUOTE A)
, which will result in the symbol A
being returned upon
evaluation.
For a more real-world example that is actually useful, let’s take a
look at DEFUN
from the standard library.
(macro defun (name args docstring . body)
"Define a named lambda function with a given docstring."
`(define ,name (lambda ,args ,@body) ,docstring))
As you can see, this macro takes 3 fixed arguments followed by any
number of arguments following passed as a list bound to BODY
. The
first argument, name, is within a quasiquoted expression, but contains
an unquote special-form operator. This causes it to be evaluated during
macro expansion, resulting in the passed argument. The same thing
happens with ARGS
and DOCSTRING
. When it comes to BODY
, though,
things change. As BODY
is a list, and a function body is not a list,
but a sequence, we must transform it somehow. This is where the
UNQUOTE-SPLICING
operator comes into play, as it will take each
element of a given list and splice it into a sequence.
,BODY = ((print a) (print b) (print c)) ,@BODY = (print a) (print b) (print c)
This allows the LAMBDA
body argument to be a valid sequence of
expressions that can be evaluated properly.
When including the standard library, DEFMACRO
operates exactly the
same as MACRO
.
When the environment variable DEBUG/MACRO
is non-nil, extra output
concerning macros is produced.
Special forms are hard-coded symbols that go in the operator position. They are the most fundamental building blocks of how LITE LISP operates.
Here is a list of all of the special forms currently in LITE LISP.
- QUOTE
- Pass one and only argument through without evaluating it.
There is also a short-form built in to the parser:
'
(single quote). This allows code to be written much faster, as quoting is something that happens quite often in the land of LISP.'X == (QUOTE X)
- DEFINE and SET
- Bind a symbol to a given atomic value within the
LISP environment.
(DEFINE SYMBOL VALUE [DOCSTRING])
(SET SYMBOL VALUE [DOCSTRING])
DEFINE
first checks all parent environments for a binding of the symbol, and will override that one if it finds it. IfSYMBOL
is not bound in any parent environment,DEFINE
binds it in the local environment. That is, the environmentDEFINE
was called from.SET
only operates on the global environment. This environment is the top level environment that is carried between evaluations, whereas local environments tend to go away after evaluation completes. - LAMBDA
- Create a closure from the given expected arguments and body.
(LAMBDA ARGS BODY)
This is an expression which returns a closure. A closure is just like a function, except that it retains a pointer to the environment that it was created within, allowing any variable accesses to be resolved as expected.
This closure can be placed directly in the operator position and called. Any arguments following the operator position are arguments to the given operator. An error will be reported if the number of arguments does not match, unless making use of an improper list to gather all remaining arguments into one.
((identity (x) x) 42)
- IF
- A conditional expression.
(IF CONDITION THEN OTHERWISE)
Evaluate the given condition. If result is non-nil, evaluate the second argument given. Otherwise, evaluate the third argument.
- WHILE
- A conditional loop.
(WHILE CONDITION BODY)
Evaluate condition. If result is non-nil, evaluate BODY one time. Repeat each time body is evaluated.
Extra information regarding
WHILE
loops is output when theDEBUG/WHILE
debug flag is set to a non-nil value. - PROGN
- Evaluate sequence of expressions, returning result of last expression.
This is mainly used within
IF
to be able to evaluate multiple expressions within theTHEN
orOTHERWISE
singular expression argument. - MACRO
- Create a closure, except the passed arguments are not
evaluated, and the value returned from the macro is evaluated,
then that return value is the result.
(MACRO SYMBOL ARGS DOCSTRING BODY)
One of the most useful features of macros is quasiquoting, which is just a fancy word meaning evaluating only some arguments while passing others through quoted. See the section on macros for more details.
- EVALUATE
- Return the result of the given argument after evaluating it.
This is mostly used in macros to evaluate certain arguments more than once.
- ENV
- Return the current environment.
The first element (car (env)) is the parent environment. If the parent environment is
nil
, that indicates the environment is the global environment.NOTE: This is a copy of the environment. Changes to it are not reflected in the environment itself.
- ERROR
- Print the given message to standard out after an error indicator. Returns the given message. Halts evaluation.
- QUIT-COMPLETELY
- Exit LITE entirely. Shut down the program.
Return STATUS code from program, or
0
if one isn’t given.(QUIT-COMPLETELY [STATUS])
Default binding:
CTRL-ALT-Q
- AND
- Return nil as soon as one of the arguments evaluates to nil.
Otherwise return
T
. - OR
- Return
T
as soon as one of the arguments evaluates non-nil. Otherwise return nil.
Structures are defined in the standard library, and can not be used unless it is included.
In LITE LISP, structures are basically an associative list with stricter rules.
Each association within the structure is referred to as a member.
Each member must be a pair with a symbol on the left side. This symbol is the member’s identifier, or ID.
Let’s look at how to define a new structure:
(defstruct my-struct
"my docstring"
((my-member 0)))
Here, we have a structure, my-struct
, with a single member, my-member
.
It should be noted that the syntax for defining members matches let
exactly, at least on the surface. One important thing to note is that
initial values given to members are not evaluated, and so must be a
self-evaluating value (a literal). For example, attempting to put the
name of a function as an initial value does not work (at least not as
expected). The member will be bound to the symbol that matches the name
of the function, not the function itself.
To access the value of any given member within a structure, use get-member
:
(get-member my-struct my-member)
This will return the value of the member with an ID of my-member
within my-struct
. If one does not exist, it will return nil. Because
we gave the member an initial value of zero, that is what is returned.
set-member
can be used to update a member’s value.
(set-member my-struct 'my-member 42)
To define a member to a function, you must first define the structure.
Afterwards, use set-member
, which evaluates the value argument:
(set-member my-struct 'my-member +)
At this point, my-member
of my-struct
has a value of the closure
which was bound to the symbol +
.
We can now call this member function using the call-member
macro:
(call-member my-struct my-member 34 35)
Any arguments after the structure symbol and member ID are passed through to the called function.
As you may already be thinking, you don’t always want to use structures
in the way shown above, where the actual structure definition is the
mutable data. In most cases, it is preferable to define a structure
once, and have multiple instances of the defined. This is possible with
the make
macro:
(defstruct vector3
"A vector of three integers, X, Y, and Z."
((x 0) (y 0) (z 0)))
;; Create an instance of a defined structure.
(set my-coordinates (make vector3))
;; Setting member values.
(set-member my-coordinates 'x 24)
(set-member my-coordinates 'y 34)
(set-member my-coordinates 'z 11)
;; Print the instance of the structure to standard out.
(print my-coordinates)
;; Access all the members of a struct using the `ACCESS` macro.
;; It is like `LET`, except it binds all of a structure's arguments
;; to their values, then evaluates the given body.
(access my-coordinates
(print x)
(print y)
(print z))
;; Accessing member IDs and values as separate lists.
(let ((coordinate-members (map car my-coordinates))
(coordinate-values (map cadr my-coordinates)))
(print coordinate-members)
(print coordinate-values))
;; Print the sum of all of the values in the structure.
(print (foldl + 0 (map cadr my-coordinates)))
- Buffer Table
Get the current buffer table with the
BUF
operator. - Symbol Table
Get the current symbol table with the
SYM
operator.Alternatively, visualize the environment by setting
DEBUG/ENVIRONMENT
to any non-nil value. - Closure Environment Syntax
Currently, closures are stored in the environment with the following syntax:
(ENVIRONMENT (ARGUMENT ...) BODY-EXPRESSION)
- Escape Sequences within Strings
Currently, strings have a double-backslash escape sequence.
The following escape sequences are recognized within strings:
\\_
-> nothing\\r
->\r
(0xd)\\n
->\n
(0xa)\\"
->"
- Debug Environment Variables
There are environment variables that cause LITE to report output extra information regarding the topic the variable pertains to when non-nil.
For a list of all debug variables that LITE internally responds to, see the file that enables all of them at once,
lisp/dbg.lt
.