# DESIGN
This document records architectural decisions for **nut-shell**. It explains why key design choices were made and what alternatives were rejected.
**When to use this document:**
- Understanding why a design decision was made
- Learning feature gating techniques for new features
- Evaluating trade-offs between architectural alternatives
## Table of Contents
1. [Core Architecture Decisions](#core-architecture-decisions)
- Metadata/Execution Separation Pattern
- Authentication: Opt-In Security
- Completion/History: Opt-Out UX
- Node Type System
- `CharIo` Buffering Model
2. [Feature Gating](#feature-gating)
- Stub Functions
- Conditional State
---
## Core Architecture Decisions
### 1. Metadata/Execution Separation Pattern
**Decision**: Separate command metadata (const in ROM) from execution logic (generic trait)
**Why**: Solves the async command type system problem - function pointers can't store async functions because each async fn has a unique `impl Future` type that can't be const-initialized.
**Benefits:**
| **Metadata** | `CommandMeta` const structs in ROM | Zero RAM usage, const-initializable |
| **Async support** | `CommandHandler` trait with async methods | No heap allocation required |
| **Codebase** | Single implementation for sync/async | No code duplication |
| **Performance** | Monomorphization at compile time | Zero-cost abstraction |
**Architecture:**
```rust
// Metadata (const-initializable, in ROM)
pub struct CommandMeta<L: AccessLevel> {
pub id: &'static str, // Unique identifier for handler dispatch
pub name: &'static str, // Display name (can duplicate)
pub description: &'static str,
pub access_level: L,
pub kind: CommandKind, // Sync or Async marker
pub min_args: usize,
pub max_args: usize,
}
// Execution logic (generic trait)
pub trait CommandHandler<C: ShellConfig> {
fn execute_sync(&self, id: &str, args: &[&str]) -> Result<Response<C>, CliError>;
#[cfg(feature = "async")]
async fn execute_async(&self, id: &str, args: &[&str]) -> Result<Response<C>, CliError>;
}
// Shell generic over handler and config
pub struct Shell<'tree, L, IO, H, C>
where
H: CommandHandler<C>,
C: ShellConfig,
{ ... }
```
**Alternatives Rejected:**
| Function pointers only | Can't store async functions (each has unique `impl Future` type) |
| Enum with Async variant | Can't const-initialize `impl Future` types |
| Async trait with `Pin<Box>` | Requires heap allocation (unavailable in `no_std`) |
| Two separate libraries | 90%+ code duplication, maintenance burden |
**Trade-offs:**
- ✅ Command name duplication (tree metadata + handler match) → explicit, debuggable
- ✅ Additional generics `H: CommandHandler<C>` → zero runtime cost via monomorphization
- ✅ Manual match statements in handler → explicit dispatch, clear control flow
### 2. Authentication: Opt-In Security
**Decision**: Optional authentication via unified architecture pattern
**Why**:
- Development/lab environments don't need authentication overhead
- Production systems require explicit security choices (not hidden defaults)
- Multiple credential storage backends needed (build-time, flash, external)
**Implementation**: Uses **conditional state** technique (see Feature Gating below). Core state fields (`current_user`, `state`) remain unconditional to preserve state machine integrity. Only the credential provider dependency and the `LoggedOut` state variant are feature-gated. When auth is disabled, `activate()` transitions directly to `LoggedIn`.
**Alternative Rejected**: Separate implementations for auth-enabled/disabled → code duplication, maintenance burden
**See also**: [SECURITY.md](SECURITY.md) for complete security architecture, password hashing, and credential storage patterns
### 3. Completion/History: Opt-Out UX
**Decision**: Tab completion and command history enabled by default
**Why**:
- Better default user experience for interactive use
- Minimal overhead (see `size-analysis/` for measurements)
- Can be disabled individually for constrained environments
**Implementation**: Uses **stub functions** technique (see Feature Gating below) - identical signatures, empty results when disabled
### 4. Node Type System
**Decision**: Enum with `CommandMeta`/`Directory` variants
**Why**:
- Zero-cost dispatch via pattern matching (vs vtable overhead)
- Enables const initialization (required for ROM placement)
- Metadata-only commands (execution via separate `CommandHandler` trait)
**Alternative Rejected**: Trait objects → runtime overhead, no const init
### 5. `CharIo` Buffering Model
**Decision**: Non-async trait with explicit buffering contract
**Why**: Works in both bare-metal and async runtimes without trait complexity. Bare-metal implementations flush immediately (blocking acceptable), while async implementations buffer and flush externally. No unstable features or `async_trait` dependencies required.
**Architecture**: See [CHAR_IO.md](CHAR_IO.md) for complete buffering model details.
**Alternatives Rejected:**
| Async `CharIo` trait | Requires `async_trait` or unstable features; makes `process_char()` async with no benefit for bare-metal |
| Callback-based | Can't propagate errors; lifetime issues in `no_std`; awkward API |
---
## Feature Gating
Optional features minimize overhead when disabled through conditional compilation. Two techniques used:
| **Stub Functions** | Feature adds functionality without affecting core state machine | `completion`, `history` |
| **Conditional State** | Feature fundamentally changes control flow or requires external dependencies | `authentication` |
### Stub Functions
**When to use**: Feature adds functionality without affecting core state machine.
**Approach**: Provide identical function signatures for both enabled/disabled. Disabled version returns empty/no-op.
**Example** (completion):
```rust
// src/tree/completion.rs
#[cfg(feature = "completion")]
pub fn suggest_completions<'a, L>(node: &'a Node<L>, input: &str)
-> Result<Vec<&'a str, 32>, CliError>
{
// Search tree, match prefixes, return suggestions
}
#[cfg(not(feature = "completion"))]
pub fn suggest_completions<'a, L>(_node: &'a Node<L>, _input: &str)
-> Result<Vec<&'a str, 32>, CliError>
{
Ok(Vec::new()) // Empty - no completions available
}
// Caller in shell/mod.rs needs no #[cfg]:
let suggestions = completion::suggest_completions(node, input)?;
if suggestions.is_empty() { /* naturally handles disabled case */ }
```
**Benefits**: Single code path, caller adapts naturally to empty results, compiler eliminates stub bodies entirely.
### Conditional State
**When to use**: Feature fundamentally changes control flow or requires external dependencies.
**Approach**: Keep state-tracking fields unconditional (e.g., `current_user`, `state`). Gate only feature-specific dependencies (e.g., `credential_provider`) and state variants.
**Example** (authentication):
```rust
pub struct Shell<'tree, L, IO> {
current_user: Option<User<L>>, // Always present (None when disabled)
state: CliState, // Always present
#[cfg(feature = "authentication")]
credential_provider: &'tree dyn CredentialProvider<L>, // Only dependency gated
}
pub enum CliState {
Inactive,
#[cfg(feature = "authentication")]
LoggedOut, // State variant only exists when needed
LoggedIn,
}
```
**Benefits**: Core state machine remains intact. When auth disabled, `Shell::new()` has different signature (no provider) and `activate()` transitions directly to `LoggedIn`. Compiler eliminates `LoggedOut` branches entirely.
**Trade-off**: Requires `#[cfg]` blocks in constructors and state matching. Accepted because the alternative (duplicate state machine implementations) creates maintenance burden.
---
## See Also
- **[EXAMPLES.md](EXAMPLES.md)**: Usage examples and configuration patterns
- **[SECURITY.md](SECURITY.md)**: Authentication, access control, and security design
- **[PHILOSOPHY.md](PHILOSOPHY.md)**: Design philosophy and feature decision framework
- **[CHAR_IO.md](CHAR_IO.md)**: `CharIo` trait and platform adapter guide
- **[DEVELOPMENT.md](DEVELOPMENT.md)**: Build commands and development workflows
- **[../CLAUDE.md](../CLAUDE.md)**: AI-assisted development guidance