# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
cargo build                        # debug build
cargo build --release              # release build
cargo test                         # run all tests
cargo clippy                       # lint

# Run a specific test suite
cargo test --test parser_tests
cargo test --test scanner_tests
cargo test --test interpreter_tests

# Run a single named test within a suite
cargo test --test interpreter_tests valid_programs
cargo test --test interpreter_tests invalid_programs

# Run the interpreter
cargo run -- -s 'let x = 42; print x;'   # from string
cargo run -- -f examples/example1.nxs     # from file
cargo run -- -f main.nxs -I ./libs        # with library search path
cargo run                                  # REPL

# Fuzzing (requires nightly)
cargo +nightly fuzz run interpret
cargo +nightly fuzz run interpret -- -max_total_time=60
```

## Architecture

Nexus is a tree-walk interpreter for a DSL describing component networks. Pipeline:

```
Source text → Scanner → Tokens → Parser → AST → Interpreter → NetworkSummary
```

**`Scanner`** (`src/scanner/`) — produces a flat `Vec<Token>` (type alias `Tokens`). Single-pass, no backtracking.

**`Parser`** (`src/parser/`) — recursive-descent, produces `Vec<Stmt>` (type alias `Ast`). `parse_block()` is the central recursive entry point for block nesting (depth bounded by `MAX_EXPR_DEPTH = 50`); `parse_expr()` is the entry point for expression nesting (also bounded by `MAX_EXPR_DEPTH` via `expr_depth`); `parse_type_id()` guards type annotation nesting with the same `expr_depth` counter. All three limits prevent stack overflow in debug builds.

**`Interpreter`** (`src/interpreter/`) — tree-walk evaluator. `eval_expr` and `eval_stmt` are the dispatch roots. Key design points:

- Control flow (`break`, `continue`, `return`) propagates as `Err(InterpretError)` with special `InterpretErrorKind` variants (`BreakSignal`, `ContinueSignal`, `ReturnSignal`). Loops and function calls catch the relevant signals.
- `eval_block` must always call `env.pop_scope()` even when returning an error — the split into `eval_block` / `eval_block_body` exists specifically for this. Do not inline them back together.
- `block_depth` resets to 0 on every function call entry (in `eval_callable`) so the limit applies per-invocation, not across the whole call stack.
- `call_depth` is bounded by `MAX_CALL_DEPTH = 64`; `block_depth` by `MAX_BLOCK_DEPTH = 50`; `eval_depth` by `MAX_EVAL_DEPTH = 50`. All three reset to 0 on function call entry (in `eval_callable`) so limits apply per-invocation. Limits are calibrated for debug build stack frame sizes. The ASAN-instrumented fuzz target inflates stack frames significantly; the fuzz harness compensates by spawning a 256 MB stack thread.
- `loop_iterations` is a **cumulative** counter across all nested loops within one function invocation. It counts every iteration of every loop construct (`loop`, `while`, `for`) and fires `LoopIterationLimitExceeded` once the combined total exceeds `MAX_LOOP_ITERATIONS`. This prevents deeply nested loops from individually staying under the per-loop limit while collectively running unbounded. Resets to 0 on function call entry.
- `Environment` is a scope stack (`Vec<Scope>`); lookups scan innermost-first. Scope leaks (push without matching pop) silently degrade lookup performance from O(1) to O(n).
- `value_to_type_id` must never be called on `Value::Uninitialized` or `Value::Unit`. For arrays, it scans for the first non-`Uninitialized` element and falls back to `Number` if none exists (defensive against all-uninitialized fill arrays). Call sites that could receive such arrays guard with `value_has_uninitialized_elements` before calling, and raise a user-visible error instead of hitting the `unreachable!`.
- **Implicit `self.` resolution**: inside a `group { ... }` body, member *references* may use a bare identifier; member *declarations* still require explicit `self.NAME = expr;`. `eval_identifier` falls back to `member_stack.last()` after env lookup misses. Function calls save and restore `member_stack` via `std::mem::take` in `eval_callable`, so members do not leak into called functions or closures. `eval_closure` extends the free-variable capture loop to look up unresolved names in `member_stack.last()` as well, snapshotting member values into the closure's `captured` map at creation time. Local `let` bindings shadow members in their lexical scope (Rust-style); explicit `self.x` always refers to the member.

**`Value`** (`src/interpreter/value.rs`) — the runtime type. Notable variants: `Node`, `Group`, `PortRef`, `NodeType` (schema only, not a runtime value), `Ref` (write-through reference), `Uninitialized` (deferred init).

**`NetworkSummary`** (`src/interpreter/network.rs`) — the output of `eval()`. Built by walking the global environment after interpretation, collecting `NodeEntry`, `GroupEntry`, and `ConnectionEntry` records. Nodes and groups are addressed by canonical path (e.g. `/group/member`).

## Test conventions

- Tests are file-based: each `.nxs` file in `tests/{scanner,parser,interpreter}/{pass,fail}/` is run as a whole-program test. Pass files must succeed; fail files must produce an error at any pipeline stage.
- `pass/` test files typically end with `assert` statements that verify computed values.
- File naming: `snake_case`, e.g. `fn_basic.nxs`. Cross-feature tests use the `cross_` prefix and live in `tests/interpreter/{pass,fail}/`.
- Library companion files (used by `use` import tests) have `_lib` in their stem (e.g. `use_basic_lib.nxs`). The interpreter test harness filters them out of standalone test discovery.
- The interpreter test harness (`tests/interpreter/interpreter.rs`) uses `Source::new_with_name` with the file path so that `use` statements resolve library files relative to the test file's directory.
- Adding a test: create a `.nxs` file in the appropriate directory — no Rust code needed.

## Key constants and limits

| Constant              | Value     | Location                                                                             |
| --------------------- | --------- | ------------------------------------------------------------------------------------ |
| `MAX_LOOP_ITERATIONS` | 1 000 000 | `src/interpreter/mod.rs`                                                             |
| `MAX_CALL_DEPTH`      | 64        | `src/interpreter/mod.rs`                                                             |
| `MAX_BLOCK_DEPTH`     | 50        | `src/interpreter/mod.rs`                                                             |
| `MAX_EVAL_DEPTH`      | 50        | `src/interpreter/mod.rs`                                                             |
| `MAX_ARRAY_SIZE`      | 65 536    | `src/interpreter/mod.rs`                                                             |
| `MAX_EXPR_DEPTH`      | 50        | `src/parser/mod.rs` (block nesting via `depth`, expression nesting via `expr_depth`) |

## Error handling patterns

`InterpretErrorKind` has two categories of variants that are **not** true errors but control-flow signals: `BreakSignal(Value)`, `ContinueSignal`, `ReturnSignal(Value)`. Any code that catches errors generically with `_ => return Err(e)` is intentionally re-propagating these signals upward. Only loops and function calls should catch and consume them.

New `InterpretErrorKind` variants that represent signals (not errors) must be handled in every `match e.kind()` call site that uses a catch-all arm, or they will be silently swallowed.

## Fuzzing

The fuzz harness (`fuzz/fuzz_targets/interpret.rs`) runs the full pipeline on arbitrary bytes and **ignores** `InterpretError` — it only catches panics and `unreachable!()`. Crash artifacts land in `fuzz/artifacts/interpret/`. Slow inputs (near the iteration/depth limits) land as `slow-unit-*` files in the same directory.

When fixing a fuzz crash, add a minimal reproducer to `tests/interpreter/fail/` (or `tests/parser/fail/`) so it becomes a regression test.
