This is the manual for JitterLisp (for GNU Jitter version 0.7.312, last updated on 27 May 2024), an efficient Lisp system using a Jittery VM.
Copyright © 2017-2021 Luca Saiu
Updated in 2022 by Luca Saiu
Written by Luca Saiu.
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Front-Cover texts and with the Back-Cover text being “You have freedom to copy and modify this manual, like GNU software.”.
A copy of the GNU Free Documentation License is distributed in electronic form along with the software in the file doc/COPYING.DOC, and available on the web at the URL https://www.gnu.org/licenses/fdl.html.
All the code examples contained in this manual are released into the public domain, up to the extent of the applicable law.
The author’s personal web site https://ageinghacker.net contains his contact information.
JitterLisp is a comparatively simple and efficient Lisp system, relying on a Jittery virtual machine for compiled code.
Jitter (see The GNU Jitter Manual) was borne out of an attempt to optimize the programming language GNU epsilon (see The GNU epsilon Manual), my main long-term project. Dissatisfied with the speedup provided by a simple direct-threaded virtual machine I started exploring more advanced techniques, and the experiment eventually grew into the current virtual machine generator. A sub-project, some might argue, that got out of hand.
Still, Jitter had seen no major application yet. It needed to be stressed to find bugs and embedded in a practical system for me to discover what was missing and fill the gaps. I wanted a real application. A real programming language. A Lisp.
And the sub-sub-project has now also gotten out of hand, again branching off in its own interesting direction.
JitterLisp lives somewhere near the boundary between the end of the domain of complex toys, and where simple real Lisp systems begin; as long as we keep seeing it as an example for Jitter either classification will do. As for concrete applications it can certainly be made practical: the Lisp library can be extended, and JitterLisp itself could become both embeddable and extensible in C without unreasonable effort. If in the future I bent the system in some of these directions, as I might well do, I guess the result would be useful; but writing yet another Lisp system was never my primary goal.
Even without the ambitious scope of Jitter I consider JitterLisp useful to show the level of performance within the reach of small, clean systems. JitterLisp gets many design decisions correct but is not perfect or exceptionally clever; still it appears more efficient than every mature “dynamic language” implementation, in some cases to a spectacular degree—due more to the faults of others than to any merit of mine.
I will not even try to hide my own personal goal for JitterLisp: being
able to say that most language implementations are inexcusably
bad, as in fact they are, with a real implementation to present as
a comparison point and not just a proof of concept; and numbers.
And so here I stand, claiming my right to rub JitterLisp on everyone’s
face. How good it feels.
Every time I had to choose whether to support a feature or to omit it for simplicity my decision relied on two criteria:
In the end the relative weight associated to each of the two criteria can only be a judgment call and some grey areas will remain, no matter how I strive for fairness. In case some reader felt that I skewed the comparison to JitterLisp’s advantage by choosing to include or exclude some feature, I am open to discussion.
JitterLisp draws inspiration from both Common Lisp and Scheme, without being compatible with either. However a competent programmer should be able to write code running unchanged on top of JitterLisp, a Common Lisp system or a Scheme system, through a layer of simple macros.
Several design ideas originate from my own GNU epsilon.
The JitterLisp language is first of all a Lisp dialect, and as such will feel familiar to any programmer already mastering any other Lisp.
JitterLisp is a lexically-scoped higher-order homoiconic1 Lisp/1 featuring closures, garbage collection, quasiquoting and Turing-complete macros.
Every syntactic form in JitterLisp is an expression, and nontrivial expressions will contain sub-expressions; JitterLisp has no “statements” as a distinct syntactic category, nor special forms which are only valid at the top level. Variables are mutable by default, and iteration and recursion are both supported. Tail calls incur no space overhead except in inter-calls between compiled and interpreted code.
Typing is dynamic. JitterLisp datatypes include the empty list object, symbols, characters, conses and procedures. Non-empty lists are cdr-nested conses. At the present time fixnums are the only numeric type.
Each procedure may be either interpreted or compiled, and procedures of different kinds are allowed to call each other with no restrictions and no difference in observable behavior apart from performance. Interpreted procedures can be compiled at run time without affecting their identity as objects.
[FIXME: quoting and quasiquoting are Lispy, not epsilonian]
[FIXME: compare with standard Lisp dialects]
[FIXME: Many of the superficial features of JitterLisp, particularly at the lexical level, come from Scheme]
[FIXME: define
operating at the top level, like Common Lisp’s
defun
but differently from Scheme]
JitterLisp is more “dynamic” and more reflective than typical Lisp dialects: any expression is allowed to define, remove or alter any non-constant global binding; the Abstract Syntax Tree returned by macroexpansion and contained within each closure is a recursive data structure describing an expression in a minimalistic core language which can be interpreted, compiled, analyzed, optimized or in general used like any other data structure, at run time, in the language itself.
Exposed ASTs and reflection make it possible and natural for the compiler to be implemented as an ordinary procedure, itself written in Lisp and running on top of the interpreter or compiled by itself at run time.
These distinctly epsilonian features, while important in making the internal implementation simpler and more elegant, are meant to be invisible to the casual user. In particular user macros operate on ordinary non-AST Lisp data structures, usually through Lisp quasiquoting, and will feel instantly familiar to any Common Lisp or Emacs Lisp programmer; the fact that the core Lisp forms are themselves predefined macros expanding to ASTs may be safely ignored in almost all programs as an implementation detail.
See Interaction example for a quick demo session introducing most of the non-traditional features likely to surprise a Lisp user.
[FIXME: no modules, packages or namespaces]
[FIXME: no values
]
[FIXME: no continuations]
[FIXME: no exceptions]
[FIXME: no variadic procedures (but I do have variadic macros)]
[FIXME: no OO]
[FIXME: no stack traces]
[FIXME: no type dispatching]
[FIXME: only a small library]
JitterLisp is free software, distributed under the GNU General Public License, version 3 or later. There is no warranty whatsoever.
This manual is free documentation, distributed under the GNU Free Documentation License, version 1.3 or later.
JitterLisp is distributed along with GNU Jitter as an advanced example of its use, and must be compiled from the source code.
JitterLisp has no separate distribution or build system of its own, but is easy to build from the Jitter build directory. The build process follows the GNU conventions and supports cross-compilation.
Once compiled JitterLisp is quite friendly to use. It supports GNU-style command-line options and can work either interactively with a REPL, or by running Lisp code from text files.
JitterLisp is distributed as an example within Jitter, and can be obtained along with the Jitter source code. It is written in C and itself, and the virtual machine it relies on for compiled closures is generated by Jitter. The current web page for Jitter, containing a link to a publicly readable git repository, is on my web site at the URL https://www.gnu.org/software/jitter .
As of 2021 JitterLisp’s development takes place on the
main git branch named master
.
I welcome feedback. If you need to contact me or want to discuss about JitterLisp please see Contacting the author in The Jitter Manual.
JitterLisp belongs to Jitter’s examples. The examples are not built
by the default make
target but are easy to generate by running
make examples
from the Jitter build directory, or by running the test suite as explained below.
The Jitter test suite includes many cases based on JitterLisp, as compiled JitterLisp procedures are a convenient and realistic way of stressing Jitter and testing its generated code. Running Jitter’s test suite with
make check
(see Running the test suite in The Jitter Manual) from its build directory is sufficient to build JitterLisp as a side effect, in all of the variants (see JitterLisp executables) supported by the Jitter configuration. I highly recommend running the test suite in any case before using Jitter.
See Working with the Jitter sources in The Jitter Manual for detailed building instructions. The build system follows the modern GNU style and in particular the common practices and suggestions from the GNU Coding Standards (see The GNU Coding Standards), which sets a few additional requirements such as GNU Autoconf and GNU Automake for “developers”—with the caveat that, at the present time before I release official stable tarballs, every user has to follow the developer instructions.
Even if the dependency is not mandatory I strongly recommend the
Boehm-Demers garbage collector, packaged by every modern GNU/Linux
distributions and also easy to install from sources.
Other optional
dependencies are the GNU Readline and GNU Libtextstyle libraries.
Both dependencies are automatically checked by Jitter’s configure
script; if a library or its C headers are missing the corresponding
functionality will be disabled, or some JitterLisp variants will not be
built.
Jitter’s build system encourages a separate build directory and supports advanced features such as cross compilation, but is standard enough to just work with the usual command line
./bootstrap && ./configure && make
in most practical cases.
According to the system characteristics as detected by Jitter’s configure script and to configure-time options supplied by the user the build system will compile different versions of JitterLisp, with the intent of testing and benchmarking Jitter.
These different compiled programs will exhibit different performance profiles, but still be equivalent in terms of Lisp functionality.
One JitterLisp executable is built for each combination of the following configuration items:
a JitterLisp program may be safe and check for operand types before executing every operation, or unsafe and omit some checks. An unsafe Lisp will be more efficient, but subject to crashes in case of user error;
JitterLisp supports the Boehm-Demers garbage collector but can also allocate heap memory without ever freeing the space, either because the garbage collection library is not available or for performance testing. The two possible configuration values are called Boehm or litter, respectively.
there are currently four dispatches supported by Jitter:
switch
-dispatching,
direct threading,
minimal threading and
no threading (see Dispatches in The Jitter Manual),
each with its own performance and portability profile.
JitterLisp will run with any dispatch.
Unless some configuration choice has been disabled multiplying
the number of choices for each of the three items gives a total
of
16 combinations and therefore
16 different executables, recognizable by suffixes in
their file name.
The safety suffix may be empty or ‘--unsafe’,
the garbage collection suffix ‘--boehm’ or empty,
and the dispatch suffix one of
‘--switch’,
‘--direct-threading’,
‘--minimal-threading’ and
‘--no-threading’.
A good combination for general use will be
safe-Boehm-no-threading, implemented in the file
bin/jitterlisp--boehm--no-threading relative to Jitter’s build
directory.
A user measuring performance in a “lowest-overhead” context might
want to try bin/jitterlisp--unsafe--no-threading, paying close
attention to how much memory is used.
The shell command
ls bin/jitterlisp*
will print the full list of available JitterLisp programs.
“Default” JitterLisp executables, with no dispatch suffix in their name are also built using the best dispatch available in the current configuration. In the example above bin/jitterlisp--boehm will have the same functionality as bin/jitterlisp--boehm--no-threading.
In the following examples I will arbitrarily refer to bin/jitterlisp--boehm as the executable to run. The executable name is easy to replace with another, in case that configuration is not available; every JitterLisp executable supports the same command-line interface. The current working directory is always irrelevant when invoking any JitterLisp executable without file arguments, which makes the bin/ component of the path name safe to freely adapt as well.
The Jitter build system does not support installing examples at the present time.
It would certainly be possible to directly use Libtool and install
Jitter’s libraries and executables.
However, as a simpler alternative, a user can configure
Jitter to generate static libraries only by calling configure
with the option --disable-shared,
and then build the examples. Each JitterLisp executable will be one
self-contained file with no external run-time dependencies, easy to
copy by hand into some directory within PATH
or to another
convenient location.
If the Jitter libraries are statically linked and every dynamic library is correctly installed then the JitterLisp executables will not access any other file by default: the large Lisp initialization file and the GPL license text are in fact embedded as constants within the native compiled file image (see constant-strings).
A user can either run JitterLisp interactively via a Read-Eval-Print Loop, or use it to execute Lisp programs from any number of text files.
An interactive run is the default: simply invoking a JitterLisp executable with no arguments, for example by typing
bin/jitterlisp--boehm
at the shell prompt will print a welcome banner and then wait for the
user to type in Lisp forms; the system will evaluate one form, print
its result unless the result is #<nothing>
, and then go back to waiting
for the next form.
User input supports the line-editing facilities of GNU Readline, as long
as Jitter has been configured with Readline support. Lisp forms entered by
the user are allowed to span multiple lines.
The user can exit by closing the input, typically by pressing
C-d on an empty line on GNU and other Unix systems.
Any non-option argument on the command line is taken as the
relative path name of one Lisp file to run; JitterLisp accepts an
arbitrary number of file names on the command line, and executes them
in the given order. JitterLisp will omit the banner and, by default,
run non-interactively if it receives any file name, exiting without
executing the REPL.
A single dash ‘-’ is a non-option argument and stands for the
process standard input, from which the program will read Lisp forms.
Like the path name for a Lisp file, a -
argument will make
JitterLisp default to non-interactive mode.
Lisp forms from input files or ‘-’, in either interactive or non-interactive mode, will be evaluated without automatically printing each result, to avoid clutter in the output. Of course a Lisp program may still call I/O procedures (see Input and output) in order to print out information as desired, without such explicit output being suppressed.
A run-time error while executing a Lisp program given on the command line will cause JitterLisp to exit with failure, without processing any remaining Lisp file.
JitterLisp executables default to treating any command-line argument beginning with a dash (‘-’) as an option which affects the system behavior in one of the ways described in this section. The exceptions are arguments constituted by exactly a single dash, as explained above, or any arguments following an argument constituted by exactly two dashes (‘--’): those are interpreted as non-options.
Several JitterLisp options affect the software behavior in some way which can be negated by another option restoring the default. When both an option and its opposite are given, the last option on the command line prevails.
I call negative options the options whose only purpose is to
negate the effect of other options, restoring the default
behavior. Negative options are useful for canceling the effect of
some option which is convenient to always pass and then occasionally
override, for example as part of a shell alias or by quick
interactive line editing of a complex shell command line.
The output of --help, described below, shows negative options
marked as ‘(default)’.
JitterLisp supports the standard GNU command-line options:
Print version information and legal notices for JitterLisp, then exit with success. Since JitterLisp is distributed along with Jitter and currently lacks a version number of its own, the output will actually reflect Jitter’s version.
Print a terse description of JitterLisp’s command-line interface including the options documented here, then exit with success.
Print a list of the options documented here with no explanation, then exit with success.
Stop option processing, and interpret any following argument in the command line as a non-option. This may be useful to execute Lisp files whose path name starts with a dash.
Checking JitterLisp’s version to obtain a version number and nothing more may be of some use in shell scripts:
Print JitterLisp’s version number and exit with success. The output is in a single line with no spaces, and does not contain JitterLisp’s name or any other information.
The following options affect JitterLisp’s interaction mode:
Run in non-interactive mode, without a REPL, independently from non-option arguments.
Run in interactive mode even if non-option arguments are given; in this case start the REPL after executing the given Lisp files.
It may be useful to run JitterLisp for evaluating one or a few Lisp forms from the command line, possibly along with some Lisp program passed as a non-option argument:
Evaluate the given possibly empty sequence of forms, printing the
final result (see Sequencing).
Evaluation and printing takes place after running all the Lisp
files from non-option arguments, and before executing the REPL
(when in interactive mode).
This option does not by itself change JitterLisp’s mode to
non-interactive.
Unfortunately the quoting rules for Bourne shells make Lisp quoting and quasiquoting (see Quoting and quasiquoting) somewhat inconvenient to use from the Unix command line:
bin/jitterlisp--boehm --no-repl --eval='(let ((a 42)) `(a is ,a))' bin/jitterlisp--boehm --no-repl --eval="(let ((a 'b)) \`(a is ,a))" bin/jitterlisp--boehm --no-repl --eval='(let ((a '\''b)) `(a is ,a))' bin/jitterlisp--boehm --no-repl --eval=\'the-result-is-a-symbol
At least in simple cases a user should be able to work around such
problems by defining a macro in a Lisp file, to be used in the
--eval argument without requiring complex quoting in the
command line. An alternative is avoiding the quoting prefix notation
(see prefix-notation)
altogether, writing for example
(quote b)
instead of
'b
within single-quoted shell text.
Some options affect the way JitterLisp prints out information and let users deviate from the default behavior.
Print back #<nothing>
when it occurs as the result of a user form
(see Uniques),
in interactive use or with --eval. The default is to omit
it, as #<nothing>
represents an “uninteresting” result from a form
which is normally evaluated for its side effects.
Print Lisp data using color and font variations to make different object types easier to distinguish at a glance. The current implementation, quite crude, relies on ANSI terminal escape sequences and uses a choice of colors suitable for a black or dark background.
The default is to always use the default terminal color and font, without emitting escape sequences.
Print uninterned symbols (see uninterned-symbols) in an alternative, more compact notation using a per-symbol index rather than a memory address. The index may theoretically wrap around after allocating a very high number of uninterned symbols.
Use the cross-disassembler specified at Jitter configuration time (see Disassembly in The Jitter Manual) rather than the default native disassembler to print out native code for compiled closures (see disassembly).
The cross-disassembler is useful for running JitterLisp on an emulator
for a foreign hardware architecture, using an objdump
executable from cross-configured GNU binutils having a name different
from the objdump
program running on the host machine.
The default, reasonable for running JitterLisp without command-line options on the host architecture it was configured for, is using the native disassembler.
Normally, when compiling a closure (see Compilation) the
JitterLisp system keeps two versions of the resulting VM routine in
memory: one non-executable routine, and one executable
routine. The executable routine is necessary for executing the
closure code, and therefore will always remain in memory until
garbage-collected—or for ever, in the case of littering JitterLisp
programs. The non-executable routine, on the other hand, can be
destroyed immediately at compilation time to save memory; its only
purpose after compilation is debugging, using disassemble
and
disassemble-vm
(see Compilation and see Disassembly in The Jitter Manual).
By default JitterLisp keeps non-executable routines in memory, and garbage-collects them along with executable routines. By specifying this option only executable routines are kept, which saves memory at the cost of making debugging output less friendly if not, according to the dispatch, completely useless.
A JitterLisp executable littering memory (see litter) will by
default print a message to the standard error every time a new block
of memory is allocated. Such warnings, although distracting, are
useful to remind the user that the running program is allocating
memory from a growing heap which will not be freed, and are therefore
enabled by default.
Non-littering executables will accept and silently ignore verbose
littering options.
This option suppresses litter-allocation messages.
When this option is given with time unspecified or different from ‘no’ the REPL times interactive commands and prints a human-readable message specifying the elapsed time in seconds after each evaluation. Valid values for time are ‘no’, ‘yes’ and ‘verbose’. A time value of ‘verbose’ causes the timed command to be printed back along with the time, which is convenient when interactively running multiple commands on the same line, to be executed in sequence and timed independently.
The default is not printing timings, as with ‘no’, when the option is not given. Giving the option without specifying time is equivalent to setting time to ‘yes’.
Only commands executed by the REPL are timed: this excludes library initialization, code from Lisp files specified in the command line, and arguments from the --eval option.
These are the negative counterparts of the options above, restoring the respective default behaviors.
The last group of options is mostly meant for the development of JitterLisp itself with respect to debugging or benchmarking.
Print out debugging information about JitterLisp’s execution progress, including the full macroexpansion and evaluation of each library form executed at startup. The default it not to print such information.
This option may occasionally be useful to discover the approximate origin of a problem, such as which of several provided Lisp files caused an error.
Disable VM instruction rewriting (see Instruction rewriting in The Jitter Manual). This is only useful for debugging rewrite rules or benchmarking their effect, or theoretically for making compilation more efficient at the price of slower execution of compiled code.
Instruction rewriting is enabled by default.
Do not run the default Lisp library at initialization, exposing only the language and library features implemented in C.
The predefined Lisp globals available when using this option will be limited to the ones marked as “core macro”, and “primitive wrapper” in the reference documentation (see Lisp reference), plus the associated primitives; in particular there will be no support for high-level macro definitions (see high-level-macros), AST optimization (see AST optimization) or compilation (see Compilation). A few globals documented as “macro” have actually two separate implementations, the first in C as a “core macro”, to be used at initialization for bootstrapping the system, and then a second, cleaner or more powerful redefinition as an ordinary “macro”, in Lisp. The option --no-library exposes the core macro implementation in this case, which may differ in subtle ways from the normally available definition, and in fact is allowed to contradict the specification in this manual.
This option is meant for debugging JitterLisp itself.
The negative versions of the options above.
This section will present the main features setting JitterLisp apart from other Lisp systems to readers already familiar with other Lisp dialects.
I believe that a sufficiently motivated beginner should be able to follow this section, possibly after first understanding the fundamentals of Lisp from the beginning of the reference part of this document (see Lisp reference). I am particularly interested in feedback from readers who cannot program yet or are only weak programmers but are making a serious attempt to learn by following this route; any beginner with useful feedback to provide, positive or negative, is welcome to contact me (see Contacting the author in The Jitter Manual).
??????????? [FIXME: Write this] [FIXME: it’s a stack machine]
[FIXME: +
is a macro]
[FIXME: look at an interpreted closure]
[FIXME: macroexpand
]
[FIXME: do some simple computation over ASTs]
[FIXME: optimize explicitly]
[FIXME: optimize retroactively]
[FIXME: These are just ideas. The variable x
is unbound.]
(append '(a b c) '(1 (2 3) 4))
⇒ (a b c 1 (2 3) 4)
x
error→ unbound variable x
'x
⇒ x
(macroexpand x)
error→ unbound variable x
(macroexpand 'x)
⇒ [variable x]
(macroexpand ''x)
⇒ [literal x]
(macroexpand '''x)
⇒ [literal (quote x)]
(macroexpand ''''x)
⇒ [literal (quote (quote x))]
(define (twice n)
(* n 2))
(twice 10)
⇒ 20
twice
⇒
#<interpreted-closure () (#<u680>)
[primitive #<primitive 2* 1-ary> [variable #<u680>]]>
(define-macro (when-zero discriminand . body)
`(when (zero? ,discriminand) ,@body))
(macroexpand '(when-zero a b c))
⇒
[if [call [variable zero?] [variable a]]
[sequence [variable b]
[variable c]]
[literal #<nothing>]]
(when-zero 42 (display 'foo) (newline))
→
[if [call [variable zero?] [literal 42]]
[sequence [call [variable display] [literal foo]]
[call [variable newline]]]
[literal #<nothing>]]
⇒
#<nothing>
;; For people familiar with Common Lisp:
(define-macro (defun operator formals . body-forms)
`(define (,operator ,@formals)
,@body-forms))
(define-macro (defmacro operator formals . body-forms)
`(define-macro (,operator . ,formals) ;; FIXME: why not (,operator ,@formals) ?
,@body-forms))
;; And then:
(defun successor (n)
(+ n 1))
(defmacro when-zero (discriminand . body)
`(when (zero? ,discriminand)
,@body))
(define my-even?
(letrec ((my-even? (lambda (n)
(cond ((= n 0) #t)
((= n 1) #f)
(else (my-odd? (- n 1))))))
(my-odd? (lambda (n)
(cond ((= n 0) #f)
((= n 1) #t)
(else (my-even? (- n 1)))))))
my-even?))
(lambda () (letrec ((f 1) (g 2)) f))
⇒
#<interpreted-closure () ()
[let #<u702> [literal #<undefined>]
[let #<u703> [literal #<undefined>]
[sequence [set! #<u702> [literal 1]]
[sequence [set! #<u703> [literal 2]]
[variable #<u702>]]]]]>
(macroexpand '(begin))
⇒ [literal #<nothing>]
(macroexpand '(begin (newline)))
⇒ [call [variable newline]]
(macroexpand '(newline))
⇒ [call [variable newline]]
(macroexpand '(begin (display 42) (newline)))
⇒
[sequence [call [variable display] [literal 42]]
[call [variable newline]]]
(macroexpand '(begin (display 42) (newline) (newline)))
⇒
[sequence [call [variable display] [literal 42]]
[sequence [call [variable newline]]
[call [variable newline]]]]
[FIXME: Fill this.]
[FIXME: we will first define Lisp data structures, then show how some of them are mapped into expressions] [FIXME: see homoiconicity-footnote]
[FIXME: boxed and unboxed objects]
All Lisp data have an output notation, and some can be
read from input via a read syntax as well. The two syntaxes,
where both defined, are compatible.
For example the number 1234
, the boolean #t
and the
symbol this-is-a-symbol
are written back by the system (when they
occur as results in the REPL, or in output) as 1234
,
#t
and this-is-a-symbol
, in the same syntax available to
the user for data input. However some other objects such as
procedures can be described in the output (for example
‘#<interpreted-closure () (x-1 x-0) [primitive #<primitive cons
2-ary> [variable x-1] [variable x-0]]>’), but have no corresponding
read syntax to be directly input as immediates. Any object can still
be built programmatically.
[FIXME: Out of convenience the written notation of an object does not necessarily convey all of the information about it; for example, the memory address where an object is stored does not appear, yet is important in the execution semantics in order to check whether two objects are the same. The output notation does express the presence of substructure sharing within a single Lisp object, currently without showing more detail: an ellipsis (‘...’) within a printed object stands for another boxed Lisp sub-object, it is not specified which, that has been already printed at least once as part of object being shown. This does not distinguish between Direct-Acyclic-Graph structures, where two pointers simply converge to the same address, and pointer cycles making circular or “infinite” structures. Shared structures have no read syntax but can be built programmatically.]
It is worth to stress how the language defined in the rest of
this section describes Lisp data, and not necessarily valid
Lisp expressions. For example foo
denotes a symbol object, but
simply typing foo
at the first REPL prompt will cause an
unbound-variable error—in such a case the symbol object foo
will be macroexpanded into a variable named ‘foo’, even
when no such variable exists.
In the same way the read (and write) syntax (1 2 3)
denotes a
well-formed Lisp datum, but would macroexpand to a procedure call with
1
as the operator, immediately causing a failure on evaluation.
See Quoting and quasiquoting for information about the quoting
syntax for building expressions evaluating to literal Lisp objects.
[FIXME: Fill this.]
Every Lisp datum contains a value and a type. Given a datum it is always possible to check for its type at run time by means of some predefined procedure (see type-checking-procedures).
Since JitterLisp has, at least currently, no subtyping relation, each
object belongs to exactly one of the following types. However some
predefined procedures are provided to check whether an object belongs
to one of several related data types; for example closure?
will
recognize either an interpreted closure or a compiled closure as a
“closure”.
[FIXME: any two object are comparable by identity: see eq?
.]
[FIXME: talk about macroexpansion]
[FIXME: Fill this.] [FIXME: immutability]
A unique object important in practice is the empty list:
In read syntax it is acceptable to separate the open and closed parentheses with arbitrary whitespace or comments, in which last case the closed parentheses will be found on a separate line.
There exists exactly one empty list object, written as ()
. It
is used, in particular, as the trailing part of the of every list
spine:
see lists.
A Boolean or canonical Boolean object represents a truth value, either false or true. There are exactly two Booleans:
In practical use #f
tends to be more important than #t
,
as JitterLisp conditionals and loops distinguish between the #f
value and any other Lisp object which is taken as “true” for
the purposes of tests. A Lisp object used as a Boolean
in this extended sense is called a generalized Boolean. Of
course generalized Booleans are not necessarily unique objects.
[FIXME: a few words about the following uniques]
[FIXME: what they are used for]
A cons is a pair object containing exactly two other Lisp objects, allowed to be conses in their turn or objects of any other types. Conses are mutable, in the sense that the two components of a cons can be changed at run time while keeping the identity of the cons object.
The first object contained in a cons is called its car, the second is called its cdr.
Syntax: If a and b are two Lisp objects, then
(a . b)
is a cons containing a as its car and b as its cdr.
Let S be any nonempty sequence of the characters “a” and “d”. Then the caSr of a cons is the car of the cSr of the cons, and its cdSr is the cdr of its cSr. For example the caddr of an object is the car of the cdr of its cdr. Such composed cons selector names are convenient to use when referring to nested components of a cons, and the JitterLisp library provides functionality to access them with any S of length 1, 2 and 3—therefore, for a level of nesting up to 4.
A list is either the empty list object or a cons whose cdr is
another list.
For example ()
, (#t . ())
, (#f . (3 . ()))
and ((1 . 2) . ())
are lists;
(#t . #t)
is not a list.
A sublist of a nonempty list is either its cdr or a sublist of
its cdr.
The elements of a nonempty list are the car of the list and the
elements of its sublists.
The empty list has no sublists and no elements.
For example the list
(foo . (10 . (bar . ())))
has the three sublists
(10 . (bar . ()))
,
(bar . ())
and
()
,
and the three elements
foo
, 10
and bar
.
Given a nonempty list its first element and its first sublist can always
be found by taking, respectively, the car and the cdr of the list. Taking
the car and cdr of the first sublist yields the second element and the
second sublist of the original list, and so on.
It is easy to see how some composed cons selectors applied to a list
identify specific sublists and elements: the list sublists will be its
cdr, cddr, cdddr, cddddr, and so on; then taking the car of the list
itself and its sublists yields the list elements: therefore the first,
second, third and fourth element of a list will be respectively its car,
cadr, caddr and cadddr.
The following notation makes lists much easier to write, as it suppresses every dot on the list spine and many parentheses; it can be used on many non-list conses as well:
Compact cons notation: Let a and b be two objects. When b is either the empty list
()
or a cons, the cons(a . b)
can be written in the compact cons notation: in compact notation both the dot and the parentheses around b are omitted.JitterLisp always adopts the compact cons notation in output, whenever applicable. Its use in input is optional.
For example
(7)
is a compact way of writing (7 . ())
,
(1 2)
is a compact way of writing (1 . (2 . ()))
,
((a))
is a compact way of writing ((a . ()) . ())
,
and (10 a . #t)
is a compact way of writing the non-list
(10 . (a . #t))
.
A further syntactic convenience is available for particular conses, which are also lists:
Prefix notation: Let q be the read syntax for a Lisp object. Then:
'q
is an alternative read syntax for(quote q)
;`q
is an alternative read syntax for(quasiquote q)
;,q
is an alternative read syntax for(unquote q)
; and,@q
is an alternative read syntax for(unquote-splicing q)
.The prefix notation is recognized by the reader, but currently not emitted in output. Its use in input, again, is optional.
The prefix notation serves to express quoting and quasiquoting in a more convenient way: see Quoting and quasiquoting.
It is worth stressing that lists are not special at the language level and do not constitute a separate type: the name “list” is just a characterization of the shape of some objects, common in practice, which include the empty list object and some conses.
[FIXME: An interpreted closure can destructively change to a compiled closure (but not vice-versa) without changing its identity.]
An Abstract Syntax Tree or AST is a recursive type representing code as a data structure. ASTs only support the core language forms which remain after macroexpanding away (see macroexpansion, see Macroexpanding) every macro use: Lisp code rewritten not to use any macro turns into an AST, which can be then interpreted or compiled, or even analyzed and transformed by other Lisp code.
JitterLisp in fact contains no executor for Lisp: there are only an AST interpreter, written in C, and an AST compiler written in Lisp and translating ASTs into code for the Jittery VM. It is thanks to the reflection capabilities provided by the AST data type that it is possible to write a self-compiler, and also a self-optimizer, in the language itself.
Even if ASTs have no read syntax the JitterLisp library provides facilities to conveniently inspect and build ASTs: see AST operations.
See Abstract Syntax Trees and their semantics for a complete description of the structure of Abstract Syntax Trees and their behavior.
JitterLisp code is executed by first translating user syntax into Abstract Syntax Trees, which are easy to interpret or compile. Far from being only an implementation device, JitterLisp ASTs are also exposed to the user who may execute them (see Code execution) and also work on them just as on any other Lisp data structure.
There is no read syntax allowing the user to directly enter an AST as a literal datum; however the library includes sufficient functionality to inspect or build ASTs (see AST operations). Such functionality is used extensively in macroexpansion and compilation.
ASTs define a minimalistic programming language, operating on
Lisp data but otherwise not particularly similar to Lisp. The syntax
given here is the output syntax used by the implementation for
printing AST data, for example as the result of macroexpand
(see Macroexpanding). This syntax is designed to be simple and
unambiguous. In order to visually stand apart from user syntax it
indicates grouping by brackets (‘[’, ‘]’) rather than
parentheses.
A dot (‘.’) in the syntax below indicates the convention of
writing a sequence of zero or more entities, represented in memory as
a list, without the surrounding parentheses. For example a
valid primitive following the syntax
‘[primitive operator . operands]’
will be [primitive + [literal 2] [variable x]]
,
where [literal 2]
and [variable x]
are ASTs encoded as
the two elements of a list within the AST for the primitive.
Every AST represents an expression. Each expression has exactly one of the following forms, named after the identifier immediately following the open bracket:
[literal value]
, a literal.
The literal value may be any Lisp datum.
[variable name]
, a variable or variable reference.
The variable name must be a symbol.
[primitive operator . operands]
, a primitive or primitive use.
The primitive operator or primitive name must be a symbol which is the name of a primitive. The primitive operands must be a list of ASTs, having exactly as many elements as the arguments expected by the named primitive.
[let name bound-expression body]
, a let or block.
The name or bound variable name in a block must be a symbol. A block bound-expression must be an AST. A block body must also be an AST.
[define name expression]
, a definition or global definition.
The global definition name must be a symbol. The definition expression or defined expression must be an AST.
[set! name expression]
, an assignment.
The assignment variable name must be a symbol. The assignment expression must be an AST.
[sequence expression-0 expression-1]
, a sequence.
Each of the first expression or expression-0 and the second expression or expression-1 in a sequence must be an AST.
[if condition then-branch else-branch]
, a conditional.
The conditional condition and its two branches must all be ASTs.
[while guard body]
, a while or loop.
Each of the loop guard and body must be an AST.
[lambda formals body]
, a lambda or abstraction.
The lambda or abstraction formals or formal arguments must be a list of symbols, all different from one another. The lambda body must be an AST.
[call operator . operands]
, a call or procedure call.
The call (or procedure call) operator must be an AST. The call operands must be a list of ASTs.
The forms above specify the entirety of the AST syntax: every
JitterLisp form, before being executed, is translated into an AST having
one of the shapes described above, which in many cases will contain other
ASTs; all the language power and complexity arise solely from the
combination of those AST forms.
Conspicuously absent from the forms above is any form of
syntactic abstraction such as macros: macros serve to automatically
rewrite complex user syntax into a combination of simple ASTs, so that no
use of a macro survives the expansion process.
It is important to notice that the syntactic constraints specified
above, for example [if …]
having exactly three
subforms and the first component of [set! …]
being a
symbol, are verified at AST construction time: it is impossible to
build a syntactically incorrect AST without immediately failing.
In this sense only syntactically correct ASTs may be said to “exist”.
JitterLisp ASTs constitute an eager imperative call-by-value language based on named, lexically scoped variables and global state.
What follows is an attempt of describing the behavior of AST evaluation in a precise way without the burden of complicated notation.
The mathematically inclined reader will see immediately how the AST semantics would be easy to model in an operational style, with logic rules having multiple premises and one conclusion. Here I will content myself with rendering such rules into more accessible English.
JitterLisp, like most modern Lisp dialects with the (now partial) exception of Emacs Lisp, is lexically scoped. This means that a variable use refers to the innermost variable binding with the same name which syntactically contains the variable AST as a sub-AST. This reference may be different from the most recent binding in time still in force for the same variable—which is used in “dynamically scoped” languages, but not in JitterLisp.
An environment is a mapping from each variable name to its associated value. During the evaluation of an AST some environment is always implicitly in force, which dictates the current value of every variable.
Variable scopes can be nested, adding bindings to the local environment which are visible by variable references until the scope ends. An inner block or call may shadow a variable binding, by re-binding a variable with the same name to a possibly new value and thus making the original variable temporarily inaccessible, as long as the new environment re-binding the same variable is in force.
Some variables may be globally bound: the global binding for a variable records the default value of a variable to be used when a variable is not bound in the currently active environment.
JitterLisp treats failure or errors in a trivial way.
If evaluation fails at any point out of the REPL, then the error is fatal: the execution ends, and the JitterLisp program exits reporting failure.
If evaluation fails when evaluating a form entered at the REPL, then the error is equally unrecoverable, and the AST evaluation terminates reporting failure; however the REPL is not exited, and the user has the opportunity to try and execute other forms in a state altered by the changes which were executed with success before the error, starting again in an empty local environment.
JitterLisp currently provides no exception system, either at the AST
level or as part of higher layers.
The error
primitive (see Erroring out) provides a way to
explicitly fail under program control, but it is not possible to
recover from such a failure with any form similar to “catch
”
or “try
” as they exist in other Lisp dialects or in other
languages.
An AST is evaluated in a given environment, which is empty at the top level.
Evaluating an AST will either fail or yield exactly one value, allowed to have any JitterLisp data type (see Lisp data), as the result. [FIXME: non-termination]
In the following, particularly within examples, I will use the following notation:
Evaluation notation: form, environment ⇒ result [-| output]
form, environment error→
To mean, respectively, that a given form evaluated with a given environment either returns a result (possibly with an output side effect), or fails. An environment, as stated already, is a mapping from variables to values.
[FIXME: Update the evaluation notation and the description below: a form should return a new environment as the result, and possibly a new global environment as well.] [FIXME: distinguish environment extension from environment update. This is important for let versus set!, below.]
ASTs must be finite in size and non-circular2; however different parts of the same AST are allowed to share structure, as long as AST objects in memory form a DAG and not a cyclic graph.
[literal value]
Evaluating a literal AST yields its value as the result, as contained within the AST and independently from its type, without any further processing, independently from the environment. The evaluation of a literal AST always terminates and never fails.
- Examples:
[literal #t]
, {} ⇒#t
[literal (a . 42)]
, {} ⇒(a . 42)
[literal a]
, {} ⇒a
[literal a]
, {a
=42
} ⇒a
[literal [literal a]]
, {} ⇒[literal a]
[variable name]
If the named variable is bound in the current environment, then the result is its value in the environment; otherwise, if the variable is not bound in the environment but is bound globally, the result is its global value. If the variable is unbound both in the environment and globally then the form evaluation fails.
- Examples:
[variable a]
, {} ⇒4
ifa
is globally bound to4
[variable a]
, {} error→ ifa
is globally unbound[variable a]
, {a
=7
} ⇒7
independently from global bindings
[primitive operator . operands]
Evaluating a primitive begins by evaluating the operands, in a strict left-to-right order, to obtain the operand values as results. Then, unless evaluation has failed already on the operands, the result is the result of applying the primitive to the operand values, according to the primitive behavior. This primitive application may fail, for example if the operand values do not have the expected types, in which case the evaluation of the entire AST fails.
- Examples:
[primitive 1+ [literal 42]]
, {} ⇒43
[primitive negate [variable a]]
, {a
=7
} ⇒-7
[primitive 1+ [variable a]]
, {} error→ ifa
is globally unbound or bound to a non-number[primitive 1+ [literal a]]
, {} error→[primitive display [literal 42]]
, {} ⇒#<nothing>
-|42
[let name bound-expression body]
A block serves to introduce a local variable, whose binding is visible within the body.
Evaluating a block starts with the evaluation of the bound expression in the current environment, whose result is the bound value. Then the block body is evaluated in an environment extended by binding the bound variable to the bound value: the result of this evaluation of the body is the result of evaluation of the block.
If evaluating either the bound expression or the body fails, then the evaluation of the entire block fails.
- Examples:
[let a [literal 42] [primitive 1+ [variable a]]]
, {} ⇒43
[let a [primitive 1+ [literal 42]] [primitive negate [variable a]]]
, {} ⇒-43
[let a [literal 42] [let a [literal #t] [variable a]]]
, {} ⇒#t
[define name expression]
A global definition serves to alter the global binding of a variable.
In order to evaluate a global definition we first evaluate its
expression in the current environment, obtaining the defined
value. Then we alter the global binding for the named variable,
setting it to the defined value. Only the global binding
is ever altered by define
, even if the variable happens to
be bound locally at the time of the definition.
A global definition fails if either the expression fails or the named variable has already been made constant (see constants).
The result of a global definition AST is #<nothing>
.
[set! name expression]
An assignment destructively modifies the given environment, replacing the current value for the name variable with another. This altered binding is visible in the following code using the same environment.
An assignment evaluation starts by evaluating the expression in the given environment, yielding the assigned value as result. Then, unless the expression evaluation failed, we alter a binding. If the named variable is locally bound, then the local environment is modified by changing the binding for the named variable to the assigned value; otherwise, if the named variable is globally bound, we set its global binding to the assigned value.
Evaluating an assignment AST fails when evaluating the expression fails, or because the variable is unbound (both locally and globally) at the time of the assignment, or because the variable is locally unbound and globally bound as a constant.
The result of an assignment AST is #<nothing>
.
[sequence expression-0 expression-1]
A sequence serves to evaluate two ASTs one after the other. The fact that the sub-ASTs of a sequence are exactly two is not a limitation, since by nesting sequences one inside the other, it is possible to evaluate any number of ASTs.
Evaluation starts by evaluating the first expression in the given environment, ignoring its result. Then, in the same environment, the second expression is evaluated; its result will be the result of the sequence AST evaluation.
Evaluating a sequence fails if evaluating either the first or the second expression fails.
[if condition then-branch else-branch]
[FIXME: write]
[while guard body]
[FIXME: write]
[lambda formals body]
[FIXME: write]
[call operator . operands]
[FIXME: write]
At this point, in principle, it would be possible to describe how each individual JitterLisp form expands into ASTs.
Except in interesting or particularly subtle cases, I will avoid such pedantry.
In fact one of the justifications for having a core-based language like JitterLisp (and epsilon) is that once the core and the expansion mechanisms are specified in a sufficiently formal way, every language extension automatically inherits the mathematical precision of its definition; the source code of each macro can be, I argue, a rigorous enough specification.
Not more than a handful of macros called core macros are
defined in C, and even those
should present no particular difficulty. For example begin
(see Sequencing) is a core macro, defined in
jitterlisp-macros.c:
/* Return the expansion of the given list of forms as a sequence. This is what the primitive macro begin does and the same code is used whenever a form sequence is allowed, for example in a lambda body. */ static jitterlisp_object jitterlisp_macroexpand_begin (jitterlisp_object forms, jitterlisp_object env) { /* Zero-form body. */ if (JITTERLISP_IS_EMPTY_LIST (forms)) return jitterlisp_ast_make_literal (JITTERLISP_NOTHING); /* Ill-formed body. */ if (! JITTERLISP_IS_CONS (forms)) jitterlisp_error_cloned ("begin: non-list body"); /* At this point we have to look inside the cons. */ jitterlisp_object first = JITTERLISP_EXP_C_A_CAR(forms); jitterlisp_object rest = JITTERLISP_EXP_C_A_CDR(forms); /* One-form body. */ if (JITTERLISP_IS_EMPTY_LIST (rest)) return jitterlisp_macroexpand (first, env); /* Multiple-form body. */ return jitterlisp_ast_make_sequence (jitterlisp_macroexpand (first, env), jitterlisp_macroexpand_begin (rest, env)); }
The code shows that the sub-forms within a begin
form are
recursively expanded in right-nested sequence ASTs, and that there are
special cases for a begin
with zero or one sub-forms. It is
easy to check that this is indeed the case, by observing the ASTs
generated by macroexpand
from begin
forms at the REPL:
(macroexpand '(begin)) ⇒ [literal #<nothing>] (macroexpand '(begin 42)) ⇒ [literal 42] (macroexpand '(begin (display 42))) ⇒ [call [variable display] [literal 42]] (macroexpand '(begin (display 42) (newline))) ⇒ [sequence [call [variable display] [literal 42]] [call [variable newline]]] (macroexpand '(begin (display 42) (newline) (display 43) (newline))) ⇒ [sequence [call [variable display] [literal 42]] [sequence [call [variable newline]] [sequence [call [variable display] [literal 43]] [call [variable newline]]]]]
The description above should be sufficient for a motivated reader,
along with macroexpand
and the informal description in the
following, to follow the details of every macro definition in the
implementation.
[FIXME: Fill this.] [FIXME: ASTs are validated at construction time: the implementation never needs to worry about whether an already existing AST is syntactically well-formed; this makes interpretation more efficient and other things simpler.]
[FIXME: Write this.]
The distinction between language and library is not clear-cut in a system like JitterLisp with strong syntactic abstraction, where almost all of the language is implemented in itself. Here I present the linguistic features which are “fundamental” in a conceptual way, independently from the way they are implemented.
Users interested in distinguishing which individual feature is implemented in C rather than as a Lisp macro may check the tag (“macro” or “core macro”, and only occasionally in this chapter “primitive wrapper”) in the documentation of each item and then follow the sources, with the caveat that a few (important) forms are implemented more than once: first by a temporary definition in C, in order to run the library and initialize the global state; then, from the library itself, the feature is re-defined in Lisp in a more general or powerful way.
[FIXME: Write this]
[FIXME: lexical scoping]
[FIXME: left-to-right]
[FIXME: case sensitive]
[FIXME: metasyntactic conventions: the dot]
[FIXME: erroring out]
[FIXME: Write something]
[FIXME: There is a lot of redundancy, by design. It is desirable to use the most specific form available, for readability and occasionally also for efficiency’s sake.]
[FIXME: dot notation, again, like in AST syntax: here, however, we are speaking of user syntax with Lisp conses.]
[FIXME: Write this]
There is currently no syntactic support for local macros, such as
Common Lisp’s macrolet
.
[FIXME: sequencing is nice]
[FIXME: “implicit progn
” or “implicit begin
”: I do it
in more cases than Common Lisp and Scheme]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
begin-1
is only provided for symmetry, and is functionally
equivalent to begin
with the restriction of requiring at least
one form.
A conditional form consists in evaluating some condition form and then decide which form to evaluate next according to the result of the condition.
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
A block introduces a scope where some variables are locally bound.
Following a lexical scoping discipline a variable binding is visible in the syntactic region where it has been bound and in every callee, as long as it is not shadowed by an inner binding for the same variable.
[FIXME: Write this]
The fourth variant, with name being a symbol, is listed here out of completeness
but belongs with looping forms; see named let
for its description.
[FIXME: Write this. Common Lisp calls “template” what I call “pattern”.]
An assignment operation destructively modifies an existing variable binding.
Evaluate the forms forms left-to-right, then assign the result of the last one
(or #<nothing>
if no forms were given) to the innermost binding for the variable
x.
Error out if x is not a symbol, if x is unbound, or if the binding for x is global and constant.
[FIXME: speak about constants]
Rationale: [FIXME: efficiency of compiled code]
[FIXME: write. e must evaluate to a symbol, which is allowed to be also lexically bound; however only its global binding is affected. If called on a globally unbound symbol, it becomes impossible to ever assign or define it.]
A loop form consists in executing the same body forms potentially multiple times, in sequence.
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
[FIXME: Write this]
The other, simpler, syntax for let
is described among block
forms: see let
.
[FIXME: Write this. Speak of low-level-macro-args
]
[FIXME: Write this. This uses destructuring bind
see destructuring-bind]
The macros above would suffice, along with define
, for globally
binding macros. However define-macro
is provided as a more convenient
syntax: see define-macro.
[FIXME: Write this]
[FIXME: Every symbol documented here is a global constant, except for macros; rationale: it is convenient to be able to re-define macros in a backwards-compatible way, and macros are not critical to performance as macro calls are always replaced with AST combinations before evaluation]
[FIXME: “Actual” is a terrible word to use here.]
[FIXME: Write. These return a canonical Boolean and never fail.]
[FIXME: lispy-and
is provided just for symmetry with lispy-or
,
and is in fact an alias for and
. The traditional Lisp
definition for and
introduces no inefficiencies in this
case, differently from the traditional Lisp implementation of
or
.]
[FIXME: fixnum only for the time being]
[FIXME: fixnum only for the time being]
[FIXME: quadratic]
[FIXME: yes, the things that follow are really procedures]
[FIXME: Performance caveat]
[FIXME: order caveat]
[FIXME: The word “set” as used here refers to a collection of objects in the
mathematical sense, and has nothing to do with the action of “setting” as
performed by set-thing!
procedures.]
[FIXME: No set->list
is needed].
[FIXME: if the argument is not an AST these all fail.]
[FIXME: types: when we fail]
[FIXME: these fail if the argument is not an AST or if it’s an AST of the wrong case] [FIXME: no destructive operations right now. They would be easy to add, even if using them in an unrestricted way might destroy compiler properties; anyway, it is already possible to destroy the same compiler implied properties, by violating other unenforced invariants]
[FIXME: An example showing a constuction, check and field access for the same case, to highlight the lexical conventions.]
[FIXME: There is currently no syntactic support for local macros, such as
Common Lisp’s macrolet
.]
JitterLisp provides very crude I/O functionalities. Files are not implemented, so input and output only use the standard input and standard output, respectively, or the terminal via ReadLine.
The following functionality provides a thin wrapper over C for reading or writing one character at a time.
Read a single character from the terminal and return it, or return #<eof>
if the input ends.
The implementation is a thin wrapper over the getchar
function from the C library.
Print the single character c to the standard output. Error out if c is not a character object.
Print a newline character to the standard output.
If newline
were not already provided in the library it could be defined as:
(define-constant (newline) (character-display #\newline))
Read one Lisp object in its input representation from the terminal, using the
Readline functionality if enabled, and return it.
Return #<eof>
if the input ends before an object is entered.
The input may span several lines and include whitespace or even Lisp comments, but the read object must be exactly one.
Error out on syntax error, or if a second object starts after the first.
Emit the printed representation of thing to the standard output. No newline character or other terminator is added.
Print data location information, mapping runtime VM data structures into hardware machine resources. This is convenient for reading disassemblies.
Print the names of all defective specialised instructions. Notice
that the set of defective specialised instructions may be different
from the set of replaced specialised instructions: when
any call-related specialised instruction is defective then
every call-related specialised instruction will be replaced.
The output is meant for the user.
Print information about defective specialised instruction, listing
the name of each replaced specialised instruction along with the
specialised instruction replacing it. About the difference between
defective specialised instruction and replaced specialised instruction
see ‘print-defects’.
The output is meant for the user.
Print cumulative profile information about the specialised
VM instructions run up to this point. This just prints a warning if
JitterLisp was not compiled with profiling instrumentation enabled,
defining at least one of the CPP macros JITTERLISPVM_PROFILE_COUNT
and JITTERLISPVM_PROFILE_SAMPLE
; since profiling instrumentation
incurs a performance penalty it is disabled by default.
This primitive wrapper behaves exactly like
print-profile-specialized
, except that it prints information
about unspecialised VM instructions.
Reset profile information, changing the execution count for every instruction back to zero.
[FIXME: Fill this.]
[FIXME: it’s a stack machine: two stacks plus registers. Explain calling conventions. Explain closure representations. Explain tagging, possibly at the beginning]
What follows is an alphabetical list of read syntax (see Lisp data) and predefined JitterLisp globals, without distinction between core definitions coming from the C code and library definitions from the library written in Lisp and executed at startup.
In the same sense the index glosses over the difference in type among globally defined variables: some may be procedures or primitive wrappers and other macros, from either the core or the library.
Most of the symbols named here are constant. Some, particularly macros, may not be.
[FIXME: justify; not as homoiconic as other Lisps appear, but still more than epsilon. I argue that any Lisp system aiming at decent performance must do something like that; the difference is that I expose this mechanism.]
At the time of
writing this restriction is moot. It is in fact impossible in the
current implementation to build a circular AST, for two reasons: first
of all, ASTs are immutable from JitterLisp, with the currently
existing primitives; second, letrec
(see Blocks) relies on
assignment to a variable when building circular closures, and the
strict constraint-checking policy of ASTs along with call-by-value
evaluation prevents a “temporary” value from being used inside an AST
to be built and then replaced later.