lispal
lispa, aka "LFP" or "Lisp Flavored Pascal"; is a fast, and embeddable scripting lisp that is fundamentally pascal with lisp syntax. Inspired from LFE (Lisp Flavored Erlang)
- Active · last change 8-30-2026
- ★ 2 on GitHub
README
lispal
Pascal in parentheses. Pack it and go.
lispal is a small, embeddable pascal-like language written with lisp syntax.
source files use .lfp or .lpas.
lfp stands for "Lisp Flavored Pascal", borrowing the naming idea from LFE ("Lisp Flavored Erlang").
the language keeps the parts of pascal that are useful in scripts: typed variables, constants, procedures, functions, records, enums, arrays, sets, pointers, units, and familiar control flow. the surface syntax is made entirely of s-expressions, which keeps the parser and embedding api small.
the lisp part is mostly the syntax. the language still expects you to think in pascal: explicit declarations, typed procedures and functions, records, arrays, sets, and ordinary imperative control flow. there are a few functional helpers too, where they fit.
(program hello
(const (limit integer 10))
(var (sum integer 0))
(function square ((x integer)) integer
(* x x))
(begin
(for (i 1 to limit)
(inc sum (square i)))
(writeln "sum = " sum)))source is compiled to a compact stack bytecode. on x86-64 linux, lispal then compiles every function to cached native control flow the first time it runs. each native instruction points straight at its opcode-specific implementation, so compiled code does not pass through the bytecode decoder again. integer arithmetic and comparisons have guarded fast paths; dynamic values use the same checked runtime operations as the interpreter. other platforms use the bytecode interpreter.
getting started
you need free pascal 3.2.2 or newer, make, and a c compiler. with homebrew:
brew install fpc
make checkthe 1.0 release build and binary packaging are tested on x86-64 linux. other platforms may work through the bytecode interpreter, but they are not part of the tested 1.0 release yet.
maintainers can run make distcheck to build the source archive, test from a
fresh extraction, exercise a staged install, and verify uninstall cleanup.
the build leaves the command-line runner at bin/lfp and the c library at
bin/liblispal.so.
to install for the current user under ~/.local:
./install.sh --check --userfor a system-wide install, build as your normal user and only use sudo for
copying the finished files:
make check
sudo ./install.sh --no-build --systemif the shell does not already include /usr/local/bin, enable it with:
export PATH="/usr/local/bin:$PATH"the installer also accepts --prefix, --destdir, --jobs, --no-build, and
--uninstall. the make install target is still there for packagers. after a
user install, make sure ~/.local/bin is on your path.
command line
lfp examples/hello.lfp
lfp examples/records.lpas one two three
lfp -e '(writeln (+ 20 22))'
lfp --repl
lfp --trace examples/fibonacci.lfp
lfp --dump-bytecode examples/hello.lfp
lfp --jit examples/pascal_tour.lfp
lfp --no-jit examples/pascal_tour.lfp
lfp --jit-statusthe default mode is automatic: use the native jit when it is available and
fall back to the interpreter when it is not. --jit requires the native
backend and reports an error if the host cannot provide it. --no-jit is useful
for debugging and backend comparisons. jit status also reports the number of
compiled functions, generated bytes, and native executions for that runtime.
running lfp without arguments opens the repl. scripts can also use a shebang:
#!/usr/bin/env lfp
(writeln "hello")the repl accepts multiline forms and bracketed terminal pastes as one submission.
for terminals without bracketed-paste support, enter :paste, paste the source,
then finish with :end. :history [N] lists recent forms, and :again [N]
evaluates the latest form or the selected history entry. up and down recall
single-line history in an interactive terminal.
script arguments are exposed as the immutable globals argc and argv.
argv is a zero-based lispal array containing only the arguments after the
script name.
language at a glance
names are case-insensitive, as they are in pascal. hyphens and punctuation are
allowed in identifiers, so names such as set-speed! are fine.
| pascal idea | lispal form |
|---|---|
| assignment | (set! x 42) or (:= x 42) |
| conditional | (if condition then-form else-form) |
| loop | (while condition body...) |
| repeat loop | (repeat body... (until condition)) |
| counted loop | (for (i 1 to 10) body...) |
| countdown | (for (i 10 downto 1) body...) |
| procedure | (procedure p ((x integer)) body...) |
| function | (function f ((x integer)) integer body...) |
| early return | (return value) or (exit value) |
| record field | (field person age) |
| array item | (index values 0) |
| set membership | (in value values) |
| pointer cell | (new value), (deref p), (dispose p) |
| unit import | (uses SysUtils Math) or (uses "path/to/unit") |
break, continue, case, with, labels, and goto are supported. boolean
and and or short-circuit. arithmetic and comparisons are bytecode
instructions rather than library calls. integer operations use signed 64-bit
values and report overflow as a runtime error.
the built-in scalar types are integer, int64, longint, byte, word,
cardinal, real, double, single, boolean, string, char, pointer,
and variant. declarations are checked on assignment, argument passing, and
function return.
records and enums
(type
(record person
(name string)
(age integer))
(enum color red green blue))
(var
(ada person (person "ada" 17))
(favorite color green))
(inc (field ada age))
(writeln (field ada name) " is " (field ada age))record constructors take fields in declaration order. omitted trailing values use the field type's default. records and enums are nominal types.
arrays and sets
dynamic arrays start at zero. fixed arrays keep their declared lower and upper bounds, and strings use pascal-style one-based indexing.
(var
(values array (array 10 20 30))
(table (array 1 10 integer))
(colors (set-of color) (set red green)))
(append values 40)
(inc (index values 0))
(set! (index table 1) 42)
(include colors blue)
(writeln (low table) ".." (high table))
(writeln (in green colors))set +, -, and * mean union, difference, and intersection. set comparisons
use subset and superset ordering.
pointers
pointers are checked runtime cells rather than unchecked machine addresses. this makes them useful for shared mutable values without letting a script corrupt its host.
(var (p pointer (new 41)))
(inc (deref p))
(writeln (deref p))
(dispose p)using a disposed pointer is a runtime error.
standard library
lispal has a small set of VM primitives, then builds the pascal library on top
of them in lispal itself. the rtl/ directory is ordinary .lpas source, so
you can read or change the library without rebuilding the interpreter.
the 1.0 tree ships 835 declared routines across 33 units. the everyday core is
System, SysUtils, Math, StrUtils, DateUtils, Dos, Crt, Strings,
Character, Types, and Variants. additional units cover dynamic libraries,
sockets, keyboard and mouse input, printers, turbo-pascal-style objects,
unix/linux support, code pages, windows paths, command-line options, base64,
uri parsing, heap checks, and http helpers: Dynlibs, Sockets, Keyboard,
Mouse, Printer, Objects, BaseUnix, Unix, UnixUtil, UnixCp, Linux,
WinCRT, WinDirs, GetOpts, Base64, URIParser, HeapTrc, and
HTTPProtocol.
some fpc apis need features lispal does not have yet, such as native var
records, Text file variables, RTTI object layouts, overloaded matrix objects,
or raw hardware ports. those apis are not included for now.
(program demo
(uses SysUtils StrUtils Math DateUtils)
(begin
(writeln (IntToStr 42))
(writeln (SysUtils.ChangeFileExt "demo.txt" ".lfp"))
(writeln (Math.Power 2.0 10.0))
(writeln (DaysInAMonth 2024 2))))unit names and exported names are case-insensitive. imported declarations are
available the normal pascal way as bare names, and also through qualified names
such as SysUtils.IntToStr. System is loaded automatically.
lfp looks for units beside the script, in rtl/, in the installed
lib/lfp/rtl directory, and in every directory listed in LFP_PATH. entries in
LFP_PATH use the host platform's normal path separator. both .lfp and
.lpas unit files are accepted.
the VM still provides the basic language operations directly: input/output,
scalar conversion, arithmetic, strings, collections, assert, now, and the
usual core constants. filesystem access, clocks, and other host work reach the
self-hosted RTL through private runtime primitives.
embedding
the pascal facade owns one independent runtime:
uses lfp_value, lfp_vm, lfp_embed;
var
runtime: tlispal;
begin
runtime := tlispal.create;
try
runtime.jitmode := jmauto;
runtime.setinteger('score', 100);
runtime.eval('(writeln "score = " score)');
finally
runtime.free;
end;
end;native callbacks use tlispalnativecallback and may carry an opaque host data
pointer. examples/embed_pascal.pas contains a complete callback example.
the c api is declared in include/lispal.h:
void *runtime = lfp_create();
lfp_set_jit(runtime, -1);
const char *result = lfp_eval(runtime, "(+ 40 2)", "<host>");
if (!result)
fprintf(stderr, "%s\n", lfp_last_error(runtime));
lfp_destroy(runtime);returned strings belong to the handle and remain valid until the next api call
on that handle. a handle must only be used by one thread at a time; separate
handles do not share script state and may run on separate threads. installed c
projects can use pkg-config --cflags --libs lispal.
runtime notes
the jit currently targets the x86-64 system v abi on linux. it allocates code as writable memory, finishes compilation, and then changes the mapping to read-execute before calling it. it never leaves writable executable pages.
features that only make sense in a native pascal compiler are intentionally out of scope. this includes inline assembly, raw pointer arithmetic, record packing directives, compiler directives, native file records, classes, interfaces, generics, and native abi overlays. lispal is a scripting runtime with pascal semantics, not a source-compatible free pascal compiler.
each engine owns its functions and heap objects as an arena. destroying the engine releases them together. there is no process-wide vm state and no external garbage collector.
source layout
src/lfp_value.pas values and managed heap objects
src/lfp_sexpr.pas lexer and s-expression parser
src/lfp_vm.pas compiler, bytecode vm, types, and rtl host primitives
src/lfp_jit.pas native control-flow compiler
src/lfp_embed.pas pascal embedding facade
src/lfp_capi.pas c abi shared library
src/lfp.pas command-line runner and repl
include/lispal.h public c header
rtl/ self-hosted pascal-style runtime units
examples/ scripts and embedding examples
tests/ interpreter, jit, repl, embedding, and rtl regression suitelicense
the project is available under the mit license.

