# Tulisp - Claude Code Guidelines
## Project Overview
Tulisp is an embeddable Lisp interpreter for Rust with Emacs Lisp-compatible syntax.
Zero external dependencies. Rust edition 2024, MSRV 1.88.0. License: GPL-3.0.
**Hard rules:**
- Never use `unsafe` code.
- Never add external dependencies (crates). Everything must be implemented using only the Rust standard library.
- Never use `unwrap()` or `expect()` in library code; propagate errors with `?`.
- Always run `cargo fmt` and `cargo clippy` and fix all warnings before submitting changes.
- Don't reference downstream apps' domain language in source or tests or commits.
Tulisp is a general-purpose interpreter; tests, comments, and code
should use generic names. If a downstream app (e.g. microsim) prompted
a regression test, give the test generic identifiers and describe the
*bug shape* in the comment rather than the app context. Cross-references
to specific consumers are fine in design docs (`README.md`, `TIMERS.md`,
`todo.md`, `docs/`), where they help explain motivation.
## Git identity
Commits in this repo must use:
```
Sahas Subramanian <sahas.subramanian@proton.me>
```
No `user.name` / `user.email` is configured in this clone, so pass them per-command:
```
git -c user.name='Sahas Subramanian' -c user.email='sahas.subramanian@proton.me' commit ...
git -c user.name='Sahas Subramanian' -c user.email='sahas.subramanian@proton.me' merge ...
```
## Staging files
Never use `git add -A` (or `git add .`) — this working tree often
has stray untracked files (emacs lock symlinks like `src/.#foo.rs`,
working notes like `todo.md` / `TIMERS.md` / `--bench`, editor
state under `.claude/`) that should not land in commits. Always
stage by explicit path: `git add src/foo.rs tests/tests.rs ...`.
## CI
CI runs `cargo test` and `cargo test --features sync` on push to `main` and
all PRs — both must pass before submitting changes. The release profile
enables LTO.
## Project Structure
```
src/
lib.rs # Library root, public API re-exports, test_utils module
bin/tulisp.rs # CLI binary entry point
builtin/ # Built-in Lisp functions and macros
functions/ # Individual built-in function implementations
macros.rs # Built-in macro implementations
cons.rs # Cons cell implementation
context.rs # TulispContext - main interpreter context
context/ # Context submodules (add_function, rest, plist)
error.rs # Error and ErrorKind types
eval.rs # Expression evaluator
lists.rs # List operations
macros.rs # Rust declarative macro utilities
number.rs # Numeric types
object.rs # TulispObject - core Lisp object type
object/ # Object submodules (conversions, wrappers)
parse.rs # Lisp parser
value.rs # TulispValue enum
tests/
tests.rs # Integration tests using tulisp_assert! macro
examples/ # .lisp example files
```
## Code Style
### Naming Conventions
- Public types are prefixed with `Tulisp` (`TulispContext`, `TulispObject`,
`TulispValue`) except `Error`, `ErrorKind`, `Number`, `Shared`.
- Predicate methods follow Lisp convention with a `p` suffix: `consp()`,
`listp()`, `numberp()`, `symbolp()`, `null()`. Exception: `is_truthy()`.
### Types and Generics
- `TulispObject` is the core Lisp value wrapper (reference-counted).
- `TulispValue` is the inner enum of value variants.
- Interior mutability via `SharedMut` (wraps `Rc<RefCell<T>>` or
`Arc<RwLock<T>>` when `sync` feature is enabled).
- Use `Cow<'a, TulispObject>` in eval paths for borrowing optimization.
- Implement `TryFrom`/`Into` traits for Rust-to-Lisp type conversions.
### Error Handling
- Custom `Error` type with `ErrorKind` enum (defined via `ErrorKind!` macro).
- Functions return `Result<T, Error>` using the crate's own `Error`.
- Construct errors via static methods: `Error::type_mismatch(...)`,
`Error::undefined(...)`, `Error::void_variable(...)`, etc.
- Chain `.with_trace(obj)` to add backtrace info to errors.
- Format errors for display with `error.format(&ctx)`.
### Visibility
- Use `#[doc(hidden)]` for items that must be `pub` (e.g., for macros)
but shouldn't appear in documentation.
### Performance Annotations
- `#[inline(always)]` is used on hot-path methods (object accessors, eval
helpers). Follow existing patterns when adding similar methods.
### Macros
- Heavy use of `macro_rules!` for code generation:
- `predicate_fn!` / `extractor_fn_with_err!` for TulispObject methods
- `ErrorKind!` for error variant generation
- `tulisp_assert!` for test assertions
- Prefer declarative macros over proc macros for internal code generation.
- Reference Emacs Lisp equivalents in doc comments where applicable.
## Testing Patterns
### Integration Tests (tests/tests.rs)
Use the `tulisp_assert!` macro with three modes:
```rust
tulisp_assert!(result: "(+ 1 2)", "3"); // assert eval result equality
tulisp_assert!(result_str: "(format \"%s\" 1)", "1"); // assert string result
tulisp_assert!(error: "(+ 1 \"a\")", "expected_error_substring"); // assert error
```
### Unit Test Helpers (in src/lib.rs test_utils)
```rust
eval_assert_equal(program, expected); // eval and compare objects
eval_assert(program); // eval and assert truthy
eval_assert_not(program); // eval and assert falsy
eval_assert_error(program, expected); // eval and assert error message
```
Test functions return `Result<(), Error>`.
## Features
- **`sync`**: Makes interpreter thread-safe (Arc/RwLock instead of Rc/RefCell).
Always test with and without this feature.
- **`big_functions`**: Enables additional built-in functions.
- **`etags`**: Enables TAGS file generation support.
## Implementation Notes
### Verifying Emacs compatibility
Tulisp aims for Emacs Lisp-compatible syntax, so when the spec is
ambiguous or you suspect Tulisp diverges from Emacs, check against
real Emacs — it's installed on this VM (`/usr/bin/emacs`, GNU Emacs
30.1). Use `--batch` for one-shot checks:
```bash
emacs --batch --eval '(princ (format "%S\n" (append nil 77)))'
# => 77
```
### VM dispatch loop: never `continue`
The VM's main interpreter loop in `src/bytecode/interpreter.rs`
(`run_impl_inner`) is a `while pc < program_size` with the `pc += 1`
at the very *end* of the loop body. A `continue` inside any
instruction arm skips that increment and re-enters the same
instruction forever. If an arm needs to short-circuit, fall
through with the result already set (e.g. `let result = if ... { ... } else { ... };`)
rather than `continue`-ing.
### `defun` / `defmacro` / `defvar` register at compile time, not eval time
Tulisp parses the entire program, then compiles, then runs. During
compile, every top-level `defun` registers in `bytecode.functions`
keyed by the symbol's address; redefinitions in the same compilation
unit overwrite earlier entries before any code runs. `defmacro` and
`defvar` are also evaluated during the parse pass (`parse.rs:412-430`)
so subsequent compile decisions — macro expansion, `let`'s
lexical-vs-dynamic dispatch via `is_special()` — see them. This is
*not* the Emacs `eval-buffer` model, where each top-level form runs
in declaration order; in Tulisp:
```lisp
(defun b () 1)
(defun a () (b))
(princ (a)) ; Tulisp prints 2; Emacs would print 1
(defun b () 2)
(princ (a)) ; both print 2
```
Both calls to `a` see the second `b`. This is a deliberate property
of the compile-then-run model, not a bug — don't re-flag it in audits.
Reworking this to match Emacs ordering would require moving defun
registration into a runtime instruction; see `todo.org` items `f7`
(VM-compile defmacro bodies) and `f8` (drop the TW eval path) for the
direction that work would head.