4 stable releases
Uses new Rust 2024
| 1.4.0 | Feb 9, 2026 |
|---|---|
| 1.3.2 | Feb 8, 2026 |
#629 in FFI
Used in 7 crates
(via grift_macros)
7KB
102 lines
Grift
A minimal no_std, no_alloc R7RS-compliant Scheme implementation built on a custom arena allocator. Grift demonstrates that you can build a feature-rich, garbage-collected language without requiring heap allocation — perfect for embedded systems, WebAssembly, or environments where std is unavailable.
📦 Installation
# Install the REPL
cargo install grift --features std
# Or add to your Cargo.toml for library use (no_std by default)
[dependencies]
grift = "1.2"
🚀 Quick Start
As a Library (no_std)
use grift::{Lisp, Evaluator, Value};
// Create an interpreter with a 10,000-cell arena
let lisp: Lisp<10000> = Lisp::new();
let mut eval = Evaluator::new(&lisp).unwrap();
// Evaluate expressions
let result = eval.eval_str("(+ 1 2 3)").unwrap();
Interactive REPL
# Run directly
cargo run -p grift --features std
# Or after installing
grift
Grift Lisp
> (define (factorial n)
(if (= n 0) 1
(* n (factorial (- n 1)))))
> (factorial 10)
3628800
> (map (lambda (x) (* x x)) '(1 2 3 4 5))
(1 4 9 16 25)
> (arena-stats)
(50000 127 49873 0) ; (capacity allocated free usage%)
🎯 Project Overview
This repository contains:
| Crate | Description | no_std |
|---|---|---|
grift |
Unified re-export crate (primary entry point) | ✅ default |
grift_arena |
Arena allocator with mark-and-sweep GC | ✅ |
grift_core |
Core types: Value, Builtin, StdLib, Lisp |
✅ |
grift_parser |
Lisp parser with symbol interning | ✅ |
grift_eval |
Trampolined evaluator with proper TCO | ✅ |
grift_repl |
Interactive REPL | ❌ (uses std) |
grift_macros |
Proc macros for stdlib generation | N/A |
grift_util |
Scheme-to-Rust name conversion utilities | ✅ |
grift_arena_embedded |
Hardware access for embedded targets | ✅ |
✨ Lisp Features
Core Language
| Feature | Description |
|---|---|
| Proper Tail Calls | Full TCO via trampolining — no stack overflow on deep recursion |
| Strict Evaluation | Call-by-value semantics; arguments evaluated before function application |
| Lexical Closures | First-class functions with captured environments |
| First-class Continuations | call/cc, dynamic-wind, values, call-with-values |
| Hygienic Macros | syntax-rules, syntax-case, lambda transformers with mark-based hygiene |
| Exception Handling | guard, with-exception-handler, raise, raise-continuable, error objects |
| Record Types | define-record-type with constructors, predicates, accessors, and mutators |
| Dynamic Parameters | make-parameter and parameterize with dynamic-wind integration |
| Quasiquote | quasiquote/unquote for template-based code generation |
| Pattern Matching | case for value matching, cond for conditionals |
| Mutation | set!, set-car!, set-cdr! for imperative programming |
| Garbage Collection | Mark-and-sweep GC controllable from Lisp code |
Built-in Functions
; List operations
(car '(1 2 3)) ; => 1
(cdr '(1 2 3)) ; => (2 3)
(cons 1 '(2 3)) ; => (1 2 3)
(list 1 2 3) ; => (1 2 3)
; Predicates
(null? '()) ; => #t
(pair? '(1 . 2)) ; => #t
(number? 42) ; => #t
(symbol? 'foo) ; => #t
(procedure? car) ; => #t
; Arithmetic
(+ 1 2 3 4) ; => 10
(- 10 3) ; => 7
(* 2 3 4) ; => 24
(/ 100 5) ; => 20
(modulo 17 5) ; => 2
; Comparison
(< 1 2) ; => #t
(= 5 5) ; => #t
(eq? 'a 'a) ; => #t
; Type conversions
(number->string 42) ; => "42"
(string->number "123") ; => 123
(string->number "abc") ; => #f
(symbol->string 'foo) ; => "foo"
; Error objects
(error "bad input" 42) ; raises error with message and irritants
(error-object? obj) ; => #t if obj is an error object
(error-object-message obj) ; get error message
(error-object-irritants obj) ; get error irritants
; Memory management
(gc) ; => (marked collected before)
(gc-enable) ; Enable automatic GC
(gc-disable) ; Disable automatic GC
(gc-enabled?) ; => #t or #f
(arena-stats) ; => (capacity allocated free usage%)
; Vectors (R7RS Section 6.8)
(define vec (vector 1 2 3)) ; Create vector with elements
(vector-ref vec 1) ; => 2 (get element at index 1)
(vector-set! vec 1 42) ; Set element at index 1
(vector-length vec) ; => 3
(vector? vec) ; => #t
#(1 2 3) ; Vector literal syntax
(make-vector 5 0) ; Create vector of 5 zeros
; I/O (R7RS Section 6.13)
(display "hello") ; Display value (human-readable)
(write "hello") ; Write value (machine-readable, with quotes)
(write-char #\a) ; Write a character
(newline) ; Write a newline
Special Forms
; Conditionals
(if condition then-expr else-expr)
(cond (test1 result1) (test2 result2) (else default))
(case key ((datum1) result1) ((datum2 datum3) result2) (else default))
; Definitions
(define x 42)
(define (square x) (* x x))
; Local bindings
(let ((x 1) (y 2)) (+ x y))
(let* ((x 1) (y (+ x 1))) y)
; Sequences
(begin expr1 expr2 ... exprN)
; Short-circuit boolean
(and expr1 expr2 ...)
(or expr1 expr2 ...)
; Iteration
(do ((i 0 (+ i 1)) (sum 0 (+ sum i)))
((= i 10) sum))
; Template-based code generation
(quasiquote (a b (unquote x))) ; With x=5 => (a b 5)
; Runtime evaluation
(eval '(+ 1 2)) ; => 3
(apply + '(1 2 3)) ; => 6
; Continuations and dynamic wind
(call-with-current-continuation (lambda (k) (k 42))) ; => 42
(call/cc (lambda (k) (k 42))) ; => 42
(dynamic-wind before-thunk thunk after-thunk)
(values 1 2 3)
(call-with-values (lambda () (values 1 2)) +) ; => 3
; Exception handling
(guard (exn
((error-object? exn) (error-object-message exn)))
(error "oops")) ; => "oops"
(with-exception-handler handler-proc thunk)
(raise 'an-error)
(raise-continuable 'warning)
; Dynamic parameters
(define p (make-parameter 10))
(p) ; => 10
(parameterize ((p 20))
(p)) ; => 20
; Record types
(define-record-type <point>
(make-point x y)
point?
(x point-x)
(y point-y))
🔥 Design Philosophy
1. No Heap, No Problem
The entire Lisp runs on a fixed-size arena allocated at compile time. No malloc, no Box, no Vec in the core. This makes it:
- Predictable — Memory usage is bounded and known upfront
- Portable — Works on bare metal, WASM, or any
no_stdtarget - Safe — No undefined behavior from memory allocation failures
2. Strict Evaluation with TCO
All arguments are evaluated before function application (call-by-value), with full tail-call optimization:
; Arguments evaluated before function call
(define (add x y) (+ x y))
(add (+ 1 2) (* 3 4)) ; => 15 (both args evaluated first)
; Side effects happen immediately
(define count 0)
(cons (begin (set! count 1) 'a) '())
count ; => 1 (side effect happened during cons)
; Tail-call optimization works for deep recursion
(define (sum n acc)
(if (= n 0) acc
(sum (- n 1) (+ acc n)))) ; Args evaluated before call
(sum 10000 0) ; => 50005000 (no stack overflow)
3. Only #f is False
Unlike many Lisps, we follow Scheme's truthiness model:
(if nil 'yes 'no) ; => yes (nil is truthy!)
(if '() 'yes 'no) ; => yes (empty list is truthy!)
(if 0 'yes 'no) ; => yes (zero is truthy!)
(if #f 'yes 'no) ; => no (only #f is false)
📊 Memory Management from Lisp
Control the garbage collector directly from your Lisp code:
; Check arena status
(arena-stats) ; => (50000 234 49766 0)
; ^ capacity
; ^ allocated
; ^ free
; ^ usage%
; Manual GC control
(gc-disable) ; Pause GC for performance-critical section
; ... allocate many objects ...
(gc-enable)
(gc) ; Force collection now
; => (marked collected before)
🏗️ Architecture
See the detailed architecture documents:
- ARENA_ARCHITECTURE.md — How the arena allocator works
- SYNTAX_CASE_AND_MACROS.md — Hygienic macro system and syntax-case implementation
- SCHEME_R7RS_CONFORMANCE.md — R7RS compliance status
📚 Standard Library
The standard library is defined as static Lisp code, parsed on-demand:
; List manipulation
(length '(a b c)) ; => 3
(append '(1 2) '(3 4)) ; => (1 2 3 4)
(reverse '(1 2 3)) ; => (3 2 1)
(nth 2 '(a b c d)) ; => c
; Higher-order functions
(map (lambda (x) (* x x)) '(1 2 3 4)) ; => (1 4 9 16)
(filter (lambda (x) (> x 0)) '(-1 2 -3 4)) ; => (2 4)
(fold + 0 '(1 2 3 4 5)) ; => 15
; List generation
(range 0 5) ; => (0 1 2 3 4)
(take 3 '(a b c d e)) ; => (a b c)
; Utilities
(identity 42) ; => 42
(constantly 5) ; Returns a function that always returns 5
⚠️ Gotchas and Pitfalls
Nil is NOT False!
; WRONG: Don't use nil/empty list as a false value
(if (filter (lambda (x) (> x 10)) '(1 2 3)) 'has-items 'empty)
; If filter finds nothing, it returns '() (nil), which is TRUTHY!
; This will always print 'has-items even when the list is empty!
; RIGHT: Explicitly check for #f or use null?
(if (null? (filter (lambda (x) (> x 10)) '(1 2 3))) 'empty 'has-items)
; Now it correctly checks if the result is empty
; Note: member returns #f (not nil) when not found, so this works:
(if (member 'x '(a b c)) 'found 'not-found) ; This is correct!
Arena Capacity is Fixed
; If you run out of arena space, you get an error
; Solution: Use a larger arena or trigger GC more often
(gc) ; Reclaim unreachable objects
🔧 Project Structure
grift/
├── crates/
│ ├── grift/ # Unified re-export crate (primary entry point)
│ ├── grift_arena/ # Core arena allocator (no_std, no_alloc)
│ ├── grift_core/ # Core types: Value, Builtin, StdLib, Lisp
│ ├── grift_parser/ # Lisp parser and value types (no_std)
│ ├── grift_eval/ # Trampolined evaluator (no_std)
│ ├── grift_repl/ # Interactive REPL (uses std for I/O)
│ ├── grift_macros/ # Proc macros for stdlib generation
│ ├── grift_util/ # Scheme-to-Rust name conversion utilities
│ └── grift_arena_embedded/ # Hardware access for embedded targets
├── docs/
│ ├── ARENA_ARCHITECTURE.md
│ ├── SYNTAX_CASE_AND_MACROS.md
│ └── SCHEME_R7RS_CONFORMANCE.md
└── README.md
🛠️ Development
# Build all crates
cargo build --workspace
# Run tests
cargo test --workspace
# Run the REPL in development
cargo run -p grift --features std
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.