tulisp 0.29.0

An embeddable lisp interpreter.
Documentation
# 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.