fecs: a lisp interpreter running in wasm

Repl version - Repl+Sqlite3 version

fecs is a programming language in the style of PicoLisp and implemented as a lisp interpreter running in wasm. The implementation has the basic lisp read, eval and print (REPL), garbage collection, fixedpoint big numbers, conses/lists, symbols and strings. Only features from the WebAssembly lime1 standard are used. Therefore the wasm code can run in nearly all WebAssembly tools and settings.

The name 'fecs' stands for 'FExprs', 'cells' and 'symbols', which refer to the building blocks of the PicoLisp system. 'FExprs' are functions that get passed the unevaluated arguments, and are an alternative to Lisp macros. The 'cell' in PicoLisp is the only datastructure as a cons cell of 2 pointers (fecs uses 4 bytes per pointer). And 'symbols' are dynamically bound, rather than the more common lexical binding and scoping, which makes symbols represent an actual place in memory. These and other choices make fecs a useful and small system; the size of the .wasm file is 24kB.

The repl also includes sqlite3, in its wasm form. Fecs and sqlite3 run in the same wasm process using the same shared wasm linear memory. The sql api in fecs supports prepared statements, bindings and result iterations, and database file import and export.


Built-in functions
+ - / * % */
Math on fixed point numbers without size limitation: + addition, - subtraction, / division, * multiplication, % remainder
*/ is multiplication of all but the last arguments divided by the last argument
quote '
Returns all arguments unevaluated
prog
Excutes all expressions and returns the last result
set
Set the value of a symbol or the car of a cons
setq
Set the value of an unevaluated symbol
prop
(prop 'sym 'key) Gets the property (value . key) pair from the symbol. Will insert the property with value NIL if it does not yet exist
put
(put 'sym 'key 'value) Sets the property 'key on 'sym to 'value
get
(get 'sym 'key) Gets the value of property 'key on 'sym
getl
Gets the list of properties of a symbol
name
Gets the name of a symbol or string
cons
(cons 'any 'any) Creates a cons cell with the arguments in its car and cdr
car
Gets the car of a cons cell
cdr
Gets the cdr of a cons cell
con
(con 'cons 'val) Sets the cdr of a 'cons cell to 'val
loop
(loop .. (NIL 'cond . final-exprs) .. (T 'cond . final-exprs) .. ) Looping and conditional evaluation construct. The body of the loop is executed endlessly until a top level list clause starting with NIL or T its 'cond-itional evaluates to NIL or non-NIL respectively
eval
Evaluates an expression and returns the result
let
(let (sym 'val sym 'val ...) ...) Binds the symbols consecutively to the 'val and saves their previous values. The let form returns the final result and rebinds the symbols to the saved values
rest
('(@ (rest))) In a function with variable number of arguments (using @ as the parameter), (rest) returns the list of arguments
make
(make ... (link 1) .. (link 2) .. (link 3) ...) Convenient list constructor, the example returns (1 2 3), @ contains the last cons
link
Adds an element to end of the list of the surrounding make
prin
Prints its arguments to the output channel. Strings will be printed without quotes ("") and lists without parentheses
print
Prints its arguments to the output channel in their canonical form. String will be printed with quotes ("")
pack
Concatenates its arguments into a string
chop
Turns a string into a list of single character strings
any
(any 'string) Reads the 'string into data
quit
(quit 'msg) (quit 'msg 'arg) Stops the current execution with the error 'msg
up
(up 'sym 'val) (up 'level 'sym 'val) (up 'sym) Sets or gets the binding of 'sym in the 'level-nth scope
trail
(trail) (trail T) Returns the backtrace of all function calls. With T also returns the symbol bindings for each stack
atom
Returns true T if the argument is an atom (number, symbol or string)
=
Structured equality
==
Pointer equality
sort
(sort 'list) Sorts the list, NIL is always ordered lowest, numbers sort before symbols and symbols before lists, T is always ordered highest. Sort is destructive by reusing the cells from 'list but altering their cdr's
<
Less than. Defined for all data in fecs to create the order as specified for (sort)
flip
Destructively reverses a list
box
Creates a new uninterned symbol
catch
(catch '(NIL) Prg) If the evaluation of Prg includes a (quit "any string msg") then catch will return the string quit string and @@ will be T
zap
(zap 'Symbol) Unintern the symbol from the namespace, thus the next read of 'Symbol will be a new symbol
Loaded functions from prelude
# (de 'sym 'def) (setq de '(E (set (car E) (cdr E)) ) ) (zap 'E) # (range A B) Returns a list from A upto and including B (de range (A B) (make (loop (link A) (T (= A B)) (setq A (+ A 1)) ) ) ) # (apply 'fn 'list) Applies the 'fn to the 'list of arguments (de apply E (eval (let (Ls (eval (car (cdr E))) Ls (make (loop (NIL Ls) (link (cons 'quote (car Ls))) (setq Ls (cdr Ls)) ) ) ) (cons (car E) Ls)) ) ) (zap 'E) (de list @ (rest)) # Logical and shortcuts on first NIL result. Non-NIL results are set into @ (de and E (loop (NIL (eval (car E))) (up @ @) (T (atom (setq E (cdr E))) (up @)) ) ) (zap 'E) (de or E (loop (T (eval (car E)) (up @ @)) (NIL (setq E (cdr E))) ) ) (zap 'E) (de not E (loop (NIL (eval (car E)) T) (up @ @) (NIL) ) ) (zap 'E) (de if E (loop (T (eval (car E)) (eval (car (cdr E)))) (NIL NIL (run (cdr (cdr E))))) ) (zap 'E) (de run (E) (let (Res NIL) (loop (T (== NIL E) Res) ;; don't set @ (setq Res (eval (car E))) (setq E (cdr E))) ) ) (zap 'E) (zap 'Res) (de unless E (let (Cond (eval (car E))) (loop (NIL Cond (run (cdr E))) (T T) ) ) ) (zap 'E) (zap 'Cond) (de while E (loop (NIL (eval (car E))) (run (cdr E)) ) ) (zap 'E) # (for Elem List ... (NIL Cond . Prg) ... (T Cond . Prg) ...) # (for (Idx . Elem) List ... (NIL Cond . Prg) ... (T Cond . Prg) ...) (de for E (eval (let (IdxSym NIL ItemSym (car E) ItemSym (if (atom ItemSym) # I or (Idx . X) ItemSym (setq IdxSym (car ItemSym)) (set IdxSym 0) (cdr ItemSym) ) LBox (box) Body (cdr (cdr E)) BodyTail Body ) (loop (NIL (cdr BodyTail)) (setq BodyTail (cdr BodyTail)) ) (con BodyTail (cons (list 'NIL (list 'setq LBox (list 'cdr LBox))))) (set LBox (eval (car (cdr E)))) (cons 'loop (cons (list NIL LBox T IdxSym ItemSym (atom (car E))) (cons (list 'setq ItemSym (list 'car LBox)) (if IdxSym (cons (list 'setq IdxSym (list '+ IdxSym 1)) Body ) Body) ) ) ) ) ) ) (zap 'E) # (finally Exe . Prg) Always executes Exe after Prg, also if Prg had a (quit) (de finally E (let (Res (catch '(NIL) (run (cdr E)) ) ) (if @@ (prog (eval (car E)) (quit Res) ) (prog (eval (car E)) Res) ) ) ) (zap 'E) (zap 'Res) (prin "Prelude loaded" "\n" T)
Examples
Large numbers
(apply * (range 1 40))
Symbols
(setq Lang "fecs") (set (prop 'Lang 'impl) 'wasm) (put 'Lang 'gc T) (put 'Lang 'strings "uninterned") (cons Lang (getl 'Lang))
Functions
(setq triplist '((A B C) (cons A (cons B (cons C))) ) ) (triplist 10 (/ 160 8) 30)
Vararg function
(de squarelist @ (let (Args (rest)) (make (loop (NIL Args) (link (* (car Args) (car Args))) (NIL (setq Args (cdr Args))) ) ) ) ) (squarelist 1 2 3 4)
Fexpr
(de if Exe (loop (T (eval (car Exe)) (eval (car (cdr Exe)))) (NIL NIL (run (cdr (cdr Exe)))) ) ) (if (= 4 (* 2 2)) (print "True branch" @) (print "False branch") )
Sort
(sort '(T abc NIL 0 -10 'A 4 "hello" "WORLD" (12 . "haha") '(A B "LOL") '(A B C)))
@ result
(and 10 (+ @ 2) )
Recur/recurse
(de recur recurse (run (cdr recurse)) ) (de fibo (N) (recur (N) (if (< N 3) 1 (+ (recurse (- N 1)) (recurse (- N 2)) ) ) ) ) (fibo 10)
While and @ result
(setq L (1 2 3 4 5)) (while (car L) # @ holds result of conditional (prin @ "\n") (setq L (cdr L)))
Built-in sql functions
(sdb~open string-or-symbol)
(sdb~open "new_filename.db") Creates a new sqlite3 database and returns it.
(sdb~open 'Symbol-to-store-db-in) Import an sqlite3 file by launching a file-picker and uploading the file. The 'Symbol will hold the imported database
(sdb~export Db)
Exports the db by downloading the sqlite3 file
(sdb~prepare DB "Query or statement string with perhaps @Parameter and @MoreParameters")
Creates an sqlite3 compiled prepared statement. The parameters prefixed with "@" can be bound later.
(sdb~bind S @SomeParameter @AnotherParameter ...)
Binds in the prepared statement S the @SomeParameter with the value of the @SomeParameter symbol. For each @Parameter in the prepared statement Stmt a matching symbol with the same name starting with an @ must be passed to bind
(sdb~step S ..)
(sdb~step S) Evaluates the prepared statement S once upto the next result. Returns T if a row is available, returns 'sdb~constraint if a constraint error was raised, returns NIL when there are no more results
(sdb~step Q Col1Symbol Col2Sym ..) Evaluates the prepared statement S and if a row is available the columns from the row are stored as the value in the supplied symbols. The number of supplied symbols must be equal to the number of columns in the row
(sdb~cols S)
Returns a list of the column names of the prepared statement S
(sdb~row S)
Returns a list of the column values of the result row of a successful step of prepared statement S
(sdb~finalize S)
Finalizes a prepared statement S, after which S should not be used anymore
Sqlite examples
Import an existing sqlite3 file
(sdb~open 'DB)
Create a new sqlite3 db
(setq DB (sdb~open "mydb.db"))
Export
(sdb~export DB)
Generic query for a DB
(setq Q (sdb~prepare DB "select * from sqlite_master")) (print (sdb~cols Q)) (prin "\n") (while (sdb~step Q) (print (sdb~row Q)) (prin "\n") ) # prints generic info about the tables in DB
Sports example
(setq SportsDB (sdb~open "sports.db")) (setq CreateTableStmt (sdb~prepare SportsDB "CREATE TABLE IF NOT EXISTS sports(name TEXT NOT NULL UNIQUE, players_per_side INT NOT NULL)")) (sdb~step CreateTableStmt) (setq InsertStmt (sdb~prepare SportsDB "INSERT INTO sports(name, players_per_side) VALUES (@Name, @PlayerCount)")) (let (@Name "Football" @PlayerCount 11 ) (sdb~bind InsertStmt @Name @PlayerCount) (sdb~step InsertStmt) ) (let (@Name "Football" @PlayerCount 11 ) (sdb~bind InsertStmt @Name @PlayerCount) # can't insert duplicate name due to UNIQUE constraint (if (= 'sdb~constraint (sdb~step InsertStmt)) (let (@Name "American Football" ) (sdb~bind InsertStmt @Name @PlayerCount) (sdb~step InsertStmt) ) ) ) (setq Sports '(("Basketball" . 5) ("Tennis" . 1) ) ) (for Sport Sports (let (@Name (car Sport) @PlayerCount (cdr Sport) ) (sdb~bind InsertStmt @Name @PlayerCount) (sdb~step InsertStmt) ) ) (setq QueryStmt (sdb~prepare SportsDB "SELECT name, players_per_side FROM sports")) (while (sdb~step QueryStmt Name PlayerCount) (prin "The sport " Name " has " PlayerCount " player" (if (< 1 PlayerCount) "s") " per side.\n") )
Mandelbrot
(setq DB (sdb~open "mandelbrot.db")) (setq MQ (sdb~prepare DB " -- Mandelbrot from https://sqlite.org/fiddle/ WITH RECURSIVE xaxis(x) AS (VALUES(-2.0) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2), yaxis(y) AS (VALUES(-1.0) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0), m(iter, cx, cy, x, y) AS ( SELECT 0, x, y, 0.0, 0.0 FROM xaxis, yaxis UNION ALL SELECT iter+1, cx, cy, x*x-y*y + cx, 2.0*x*y + cy FROM m WHERE (x*x + y*y) < 4.0 AND iter<28 ), m2(iter, cx, cy) AS ( SELECT max(iter), cx, cy FROM m GROUP BY cx, cy ), a(t) AS ( SELECT group_concat( substr(' .+*#', 1+min(iter/7,4), 1), '') FROM m2 GROUP BY cy ) SELECT group_concat(rtrim(t),x'0a') as Mandelbrot FROM a;")) (while (sdb~step MQ) (prin (sdb~row MQ) "\n") )

Output: Click on output to copy to input
Input: Enter to submit, Shift+Enter for newline

(c) 2026 thegeez.net