Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

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)

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

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.

lisp
(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:

bash
brew install fpc
make check

the 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:

bash
./install.sh --check --user

for a system-wide install, build as your normal user and only use sudo for copying the finished files:

bash
make check
sudo ./install.sh --no-build --system

if the shell does not already include /usr/local/bin, enable it with:

bash
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

bash
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-status

the 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:

text
#!/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

lisp
(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.

lisp
(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.

lisp
(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.

lisp
(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:

pascal
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:

c
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

text
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 suite

license

the project is available under the mit license.