Core additions: - StringInputPort / StringOutputPort — open-input-string, open-output-string, get-output-string, read/read-char/peek-char/read-line on ports - R7RS ErrorObject class — error-object?, error-object-message, error-object-irritants; guard and with-exception-handler now receive ErrorObject instances instead of raw strings - Exact rational arithmetic via Fraction — (/ 1 3) => 1/3, literal 1/3 syntax, numerator/denominator, exact/inexact conversions - Module system — (module name (export ...) body...) and (import name) / (import (name sym ...)) for selective import - define-record-type now a Python special form supporting (inherit parent) for single-inheritance with subtype predicates - Pretty-printer — pp/pretty-print with configurable line width Correctness: - R7RS letrec* body semantics for internal defines — names pre-declared before any initializer runs, enabling mutual recursion in let bodies - string->number detects #x/#b/#o/#d and 0x/0b/0o prefixes - show() guarantees decimal point in float output - number->string rounds through show() for consistent float representation Performance: - _body_env fast path: skip scan when first body form is not define/begin - Proc.has_defs flag: skip _body_env entirely for procs without internal defines - Single-form body optimization: skip begin wrapper, set expr directly Tests: 396 → 455 (+59 new tests covering all new features) |
||
|---|---|---|
| .gitignore | ||
| bench.py | ||
| CLAUDE.md | ||
| Makefile | ||
| README.md | ||
| stdlib.lsp | ||
| tests.py | ||
| uncommonlisp.py | ||
uncommonlisp
A Scheme-like Lisp interpreter in one Python file.
λ> (define (fib n)
(let loop ((a 0) (b 1) (i 0))
(if (= i n) a (loop b (+ a b) (+ i 1)))))
λ> (map fib (iota 10))
(0 1 1 2 3 5 8 13 21 34)
Usage
python3 uncommonlisp.py # interactive REPL
python3 uncommonlisp.py script.lsp # run a file
python3 uncommonlisp.py -e '(+ 1 2)' # eval an expression
What's implemented
Core language
- Full lexical scoping and closures
- Tail-call optimization (TCO) via explicit loop — deep recursion never blows the stack
- Hygienic macros via
syntax-ruleswith ellipsis (...) support define-macro/defmacrofor procedural macro transformerscall/cc(escape continuations)values/call-with-valuesdynamic-wind,guard,with-exception-handlerquasiquote/unquote/unquote-splicingwith proper nesting- R7RS internal defines → letrec* body semantics (mutual recursion in bodies)
- R7RS error objects —
error-object?error-object-messageerror-object-irritants - Exact rational arithmetic via
Fraction—(/ 1 3)→1/3,(+ 1/4 3/4)→1 - String ports —
open-input-stringopen-output-stringget-output-stringreadon ports - Module system —
module/importwith explicit export lists define-record-typewith(inherit parent)for single-inheritance
Special forms
define set! lambda λ if cond case and or when unless
begin let let* letrec letrec* named-let do
quasiquote define-macro define-syntax syntax-rules
let-syntax letrec-syntax apply eval values call/cc
dynamic-wind guard parameterize load error
module import define-record-type
Built-ins
- Arithmetic:
+-*/quotientremaindermoduloexptsqrtabsfloorceilingroundtruncateminmaxgcdlcmlogexpsincostanatannumeratordenominatorand more - Rationals:
(/ 1 3)→1/3, literal1/3syntax, exact arithmetic,exact/inexactconversion - Comparison:
=<><=>=zero?positive?negative?odd?even? - Booleans:
notboolean?boolean=? - Equality:
eq?eqv?equal? - Pairs & lists:
conscarcdrset-car!set-cdr!listlist*lengthappendreverselist-reflist-tailmemqmemvmemberassqassvassociotamapfor-eachfilterfold-leftfold-rightreduceanyeverycountflat-mapsortsort-bypartitionfindtakedroptake-whiledrop-whiletake-rightdrop-rightzipflattenconcatenatelist-tabulateunfoldand more - SRFI-1:
lastfirst–fifthdeletelset-unionlset-intersectionlset-differenceproper-list?dotted-list? - Strings:
string-lengthstring-refsubstringstring-appendstring-upcasestring-downcasestring->listlist->stringstring->symbolsymbol->stringstring->number(with#x/0x/#b/#oprefix detection)number->stringstring-containsstring-splitstring-joinstring-trimstring-replaceformatand more - Characters:
char->integerinteger->charchar-alphabetic?char-numeric?char-upcasechar-downcase - Vectors:
make-vectorvectorvector-refvector-set!vector->listlist->vector - Hash tables:
make-hash-tablehash-table-set!hash-table-refhash-table-ref/defaulthash-table-delete!hash-table-exists?hash-table-keyshash-table-valueshash-table->alisthash-table-walkand more - Type predicates:
number?integer?real?rational?string?symbol?pair?null?list?char?vector?boolean?procedure?exact?inexact? - I/O:
displaywritenewlinereadread-charpeek-charread-lineloadwith-output-to-stringopen-input-stringopen-output-stringget-output-string - Error objects:
error-object?error-object-messageerror-object-irritants - Pretty-print:
pp/pretty-print— indented output respecting line width - Python interop:
py-evalpy-execpy-importpy-callpy-attr
Prelude (loaded automatically)
when unless case while for define-record-type (with inheritance) 1+ 1- add1 sub1 square cube compose atom? range flatten string-map string-for-each call-with-string-output-port
Standard library (stdlib.lsp, load explicitly)
Syntax-rules versions of let/and/or/cond/case/do, fluid-let, receive (SRFI-8), begin0, while/until, dotimes/dolist, push!/pop!, and-let* (SRFI-2), string utilities, list utilities (sum product maximum minimum average enumerate transpose chunks interleave), numeric utilities (factorial fib prime? primes-up-to clamp), alist/hash utilities, tree utilities, simple object system, coroutines via call/cc
Examples
; Closures
(define (make-counter)
(let ((n 0))
(lambda () (set! n (+ n 1)) n)))
(define c (make-counter))
(c) ; => 1
(c) ; => 2
; Hygienic macro (syntax-rules)
(define-syntax my-or
(syntax-rules ()
((my-or) #f)
((my-or e) e)
((my-or e1 e2 ...)
(let ((t e1))
(if t t (my-or e2 ...))))))
; define-record-type
(define-record-type point
(make-point x y)
point?
(x point-x)
(y point-y set-point-y!))
(define p (make-point 3 4))
(point-x p) ; => 3
; Named let (looping)
(let loop ((i 0) (acc '()))
(if (= i 5)
(reverse acc)
(loop (+ i 1) (cons (* i i) acc))))
; => (0 1 4 9 16)
; Hash tables
(define freq
(let ((h (make-hash-table)))
(for-each (lambda (x)
(hash-table-set! h x (+ 1 (hash-table-ref/default h x 0))))
'(a b a c b a))
h))
(hash-table-ref freq 'a) ; => 3
; Tail calls — no stack overflow even at depth 1,000,000
(define (count-down n)
(if (= n 0) 'done (count-down (- n 1))))
(count-down 1000000) ; => done
; Python interop
(define re (py-import "re"))
(py-call (py-attr re 'findall) "[0-9]+" "abc123def456")
; => ["123", "456"]
Running tests
make test # run 455 tests
make test-verbose # verbose output
Running benchmarks
python3 bench.py # compare against CPython baseline
python3 bench.py -v # show result values too
File layout
uncommonlisp.py interpreter (self-contained, one file)
stdlib.lsp extended standard library (load manually)
tests.py test suite (396 tests)
bench.py benchmarks vs CPython
Makefile make test / make repl