Architecture
lash is a high-performance shell written in D, comprising approximately 194 source files and ~42,000 lines of code. It uses a client-server daemon model where a persistent backend daemon and one or more frontend terminal instances communicate over a Unix domain socket using a binary protocol.
Client-Server Model
Section titled “Client-Server Model”The backend daemon (lash -d) is a persistent, single-per-user process that owns all evaluation logic: lexing, parsing, semantic analysis, execution, plugin hosting, and hook dispatch. Frontend instances (lash) are thin view layers responsible only for terminal I/O, line editing, prompt rendering, and display.
Frontends never interpret or evaluate lash code. All intelligence lives in the backend.
┌──────────┐ Unix Socket ┌──────────────────────┐ │ Frontend │ <--- binary proto --> │ Backend Daemon │ │ (view) │ ~/.lash/lashd.sock │ lexer/parser/eval/... │ └──────────┘ └──────────────────────┘ ┌──────────┐ │ │ Frontend │ <──────────┘ └──────────┘Five-Layer Architecture
Section titled “Five-Layer Architecture”Strict downward-only dependencies. No layer may import from a layer above it.
runtime/ -> (nothing, only std library)lang/ -> runtime/protocol/ -> runtime/backend/ -> lang/, protocol/, runtime/frontend/ -> protocol/, runtime/ (NEVER imports backend/)plugins/ -> backend/plugin/base/, protocol/, runtime/| Layer | Contents |
|---|---|
runtime/ | Pure types (LashValue, Scope, LashError), OS abstractions |
lang/ | Lexer, parser, evaluator, expansion, optimizer (turbo analysis/fusion/operations/heap) |
protocol/ | Wire format (codec/encoders/decoders), transport, IPC (shm/notify), shared paths |
backend/ | Execution (pipeline/pipe/redirects/streaming/PTY/stdin/turbo), daemon, session, config, hooks, plugins, script |
frontend/ | Terminal input, raw mode, key reader, completion, history, line parsing, connection, rendering |
Binary Protocol
Section titled “Binary Protocol”Communication uses a binary protocol over the Unix domain socket at ~/.lash/lashd.sock:
[4 bytes: message length][2 bytes: message type][payload: struct bytes]Frontend and backend share D struct definitions for zero-cost serialization.
Frontend to Backend Messages
Section titled “Frontend to Backend Messages”| Message | Description |
|---|---|
Connect | Terminal size, supported display modes |
Input | User input text |
Complete | Completion request (text + cursor position) |
Signal | SIGINT, SIGTSTP, etc. |
Resize | Terminal size change |
Disconnect | Session end |
PasswordResponse | Response to password prompt |
Backend to Frontend Messages
Section titled “Backend to Frontend Messages”| Message | Description |
|---|---|
Prompt | Prompt string to display |
Output | stdout/stderr data with stream tag |
Error | Problem/Suggestion in requested format |
Completions | Completion candidates |
Highlight | Token spans for syntax highlighting |
JobStatus | Background job state changes |
ExitCode | Command exit code |
EnvNotify | Environment variable updates |
PasswordRequest | Prompt frontend for password (echo disabled) |
KeyHookConfig | List of hooked key names (type 0x91) |
KeyEventResult | Result of key hook dispatch |
Startup and Discovery
Section titled “Startup and Discovery”| Command | Description |
|---|---|
lash | Start the frontend (interactive) |
lash -d | Start the backend daemon only |
Frontend startup: Check for existing backend at ~/.lash/lashd.sock. If responsive, connect. Otherwise, auto-start the backend, wait, then connect. Send a Connect message with terminal dimensions and supported display modes. Begin the interactive session.
Backend startup: Create socket at ~/.lash/lashd.sock. Load configuration from ~/.lash/config. Load and initialize plugins from ~/.lash/plugins.conf. Accept frontend connections. Daemonize (write PID to ~/.lash/lashd.pid).
Backend shutdown: The daemon stays alive when the last frontend disconnects. Explicit shutdown via lash -d stop. On SIGTERM, clean shutdown of plugins and session data.
Backend Modules
Section titled “Backend Modules”| Module | Responsibility |
|---|---|
| Lexer | Streaming tokenizer with command/expression mode switching |
| Parser | Produces AST from token stream. Strict — fails with Problem/Suggestion errors |
| Semantic Analyzer | Pre-execution validation (undeclared variables, type checks) |
| Evaluator | AST walker, scope management, control flow, loop protection |
| Process Manager | Process spawning, job table, stream capture, exit codes. Sets LASH_DISPLAY and LASH_TUI_VERSION on spawned processes |
| Range Engine | Lazy composable ranges with bounded buffer (default 1024). Built-in methods are range adapters |
| Plugin Host | External process plugin lifecycle |
Frontend Responsibilities
Section titled “Frontend Responsibilities”The frontend is a thin view layer:
- Line editing (keystrokes, cursor, multi-line, undo)
- Prompt rendering (displays prompt string from backend)
- Tab completion UI (displays candidates from backend)
- Syntax highlighting (colorizes using token info from backend)
- Output display (stdout, stderr, errors)
- Job status display
- Signal forwarding (Ctrl+C, Ctrl+Z sent as signal messages to backend)
- Display mode handling
Data Flow Example
Section titled “Data Flow Example”Input: `cat log.txt | head -100`.json.filter(e => e["level"] == "error").take(5)
- Frontend sends
Inputmessage. - Lexer produces tokens. Parser builds AST.
- Semantic analyzer validates.
- Process Manager spawns
cat log.txt | head -100. - Range Engine composes:
stdout -> [buffer:1024] -> json -> filter -> take(5). - Backend sends
Outputmessages. ThenPromptandExitCode.