Map: repl / loop
ral/src/repl.rs is the module-tree root and exposes one verb,
run_interactive, which boots a Session and drives it to an ExitCode
(startup).
The session
The REPL is a front-end that holds no Shell: it boots an
IdentityTransport from the repl installer and speaks only the
engine protocol — every evaluation a dispatch,
every read of engine state a probe — so what it can do is exactly what the
protocol carries (engine-protocol).
session::Session (session.rs) owns the transport, the REPL’s Host
(ReplHost), the boxed `Frontend`, a pending buffer
queued for re-edit, the probed terminal, the exit code, and the
TerminalClaim — last, so it drops last (below). There is
no job table: ral does not suspend
(ral-does-not-suspend).
Session::bootis two-staged. Stage one isIdentityTransport::bootwithstartup::engine::INSTALLERSand anAttachthat configures nothing — login matters only to the boot door’s profile sourcing; thereplrecipe does what precedes rc (below). A refusal prints its sentence and exits 2. Stage two is the host’s first dispatch:Program::Hookon theSession "boot"hook, the_ral-bootdoor, applied toBoot {login, no-rc, recursion-limit, capabilities}(ral/src/boot_door.rs).capabilitiesholds absolute paths:Boot::profilesanchors each argument at the invocation cwd before the door runs, since the rc and earlier profiles maycdfirst, and the door refuses a relative one before sourcing anything.boot_door::settlereads itsEnding:Settledcarries the rc’sRcSettings, a--capabilitiesfailure isRaisedstatus 2, a profile’sexit NisExited(N), and either ends the session there. The host then applies--surfaceover the rc’s choice, keeps the output theme, and dispatchesSession "startup"if the rc registered one.runloopsiterateuntil it returnsFlow::Break— a frontendEnd, anexit, or a severed transport.iterateis one cycle: break if thesession-endedreading names a cause (SIGTERM/SIGHUP or Ctrl-\ cancelled the durable root — cancellation is one-way, so exit with the cause’s code rather than deal an unusable prompt),process::clearany residual interrupt, write the terminal title from thecwdreading, render the prompt,read, and act on theRead.Read::Lineadds to history and evaluates;Read::Editbecomes next iteration’spending;Read::Interruptjust continues, nothing being in flight at the prompt;Read::End(code)records the exit status and breaks. Afterreadreturns the session flushes deferred plugin diagnostics, the one boundary, and checkssession-endedagain before interpreting input.readis handed the transport, the prompt, the pending buffer, and on Unix the exclusiveInputreader.- The Unix ambient forwarder first calls
ControlSender::hear, cancelling the engine’s scopes synchronously, then endsInputon a root cause. Its persistent shutdown flag and repeated thread-directed interrupt cover the interval between the root check and the blocking read; the frontend returns through normal cleanup. The signal thread never exits the process or restores its terminal. evalruns one trimmed line throughexec::step, recording anexitcode sorunbreaks cleanly.- Teardown — the
workersreading, transport detach, history flush, a one-time notice naming any worker still running (host::teardown_notice, overWorkerRows), andral_core::sandbox::teardown_session(deletes the session’s per-projection AppContainer profiles on Windows; a no-op elsewhere) — lives inDrop for Session, so it covers a panic unwinding through the ownedSessionas well as the orderly exit.
session/boot.rs is the process-level setup that precedes the engine and
the frontend the rc settles on after it: setup_signals (the whole Unix
disposition table in one place — SIGINT interrupt_handler, which raises the
foreground interrupt and nothing else, SIGQUIT root-abort, SIGTERM/SIGHUP term
handler, SIGTSTP/SIGTTOU/SIGTTIN/SIGPIPE ignore — every entry set through
core’s process::ignore / process::install, the one door that snapshots the
inherited SIG_IGNs before the first change so reset_child_signals can hand
them on: the nohup rule), claim_terminal (run after
SIGTTOU is ignored, since its tcsetpgrp runs from a group not yet in the
foreground, and before SIGTTIN is: it parks the shell on SIGTTIN until it is
foregrounded (at most 64 parks, then an error asking whether ral was started in the background of a shell that has since exited; SIGTTIN is set to SIG_DFL through core’s process::default first), so ral & does not seize the terminal from a parent shell’s
current job; it answers a TerminalClaim naming the group it left, if it
left one, which hands the terminal back on drop — else a parent doing no job
control, vim’s :sh or su, stops on its next terminal read),
setup_panic_hook (restore the
pre-raw terminal state — termios on Unix, console mode on Windows — then write
a crash log, to a state-dir path resolved at install time so a changed HOME
cannot redirect it), and create_frontend.
The boot, engine-side
The repl installer’s recipe (startup::engine::boot_repl) boots the REPL
surface — the batch surface plus the [[map/repl/plugins|_ed-* doors and
the plugin load doors]] — through boot_shell, so the typechecker and the
runtime see one surface by construction; then exit hints, the
interactive flag, $TERMINAL, and the boot door’s registration. The umask
is the login’s own, set by login(1) or PAM; the shell does not touch it.
The boot door (_ral-boot, one-shot: it unregisters its own hook) does the
rest in a run, since startup files evaluate ral: source_startup_files
(config/source.rs — login profiles, then the rc), the CLI’s
--recursion-limit after the rc, --capabilities through
load::profile::apply_session_profiles (the user’s ceiling is the last word,
and a session frame it pushes survives the door’s run), and
install_default_prompt (register the default Session/"prompt" hook,
{ return "❯ " }, only when no rc prompt: key registered one). It answers
the rc’s RcSettings {edit-mode, bell, surface, theme, startup} — the only
part of the rc the host needs; everything else the rc configures lands on the
engine’s shell.
Each startup file has one contract on its return value: a profile is sourced
for its effects and returns (); the rc returns a configuration record. An
exit in either ends the session with its status; any other failure is
reported and the boot goes on. The rc goes through load::evaluate_source,
compiled against the live session and the FileId its text is registered
with, so an alias or function it defines keeps naming the rc for the whole
session. It compiles under a return contract
(ral_core::typecheck::ReturnContract): contract::declared(Form::Rc), whose
closed keyset the rc’s own returned row is held to — the same rule
within/grant options get, extended to a program’s own return value. It is
the inferred row and not the syntax that produced it, so a key misspelled
inside a spread is caught with one written out; a return carrying no row (a
Map) meets the same keyset in RcPlan::decode instead. A file that does
not compile — a parse or type error, a broken contract among the latter — is
reported and skipped: errfmt::report_failed_load prints the caret report
its Error::uncompiled carries (diagnostics), then
names the file skipped, since it does not compile, as against sourcing stopped at the error above for one that failed partway; the boot always survives
(unconditional-mode-pass), the
whole rc unapplied rather than only the offending key. A computed rc return
(a bound variable, a call) is invisible to the contract and stays on
RcPlan::decode, which meets the same keyset and field types, reading the
shell only, and refuses the whole rc on an unknown key, a malformed value, an
unknown theme key, a prompt or startup block vet_hook rejects, or an alias
HandlerEntry::vet rejects (a non-function under aliases: is pointed at
bindings:); only then does RcPlan::apply commit, with no refusal left
(a plugin that fails to load is reported and skipped, an exit propagates). An rc startup block registers as
the Session/"startup" hook, and the host dispatches it as a hook run under
Denied terminal authority — a fresh frame whose lets do not leak.
Every evaluation is a dispatch
Each line, prompt, and hook is one Run through dispatch_to_report with
the REPL host, and the engine’s door compiles, typechecks, and runs it against
the live session (a run, end to end).
exec.rs::step is the per-line entry. line_run builds the Run —
Program::Source(line), origin: SourceOrigin::named("<stdin>"), GrantStack::root(),
no wall, RunIo::Inherit, RequestedTerminalAccess::Leased,
RunStdin::Inherit — and ReplHost::dispatch sends it. Around it the
lifecycle hooks run as dispatches of their own. Before, pre_exec folds the
plugins’ pre-exec hooks over the typed line in load order, each dispatched
with {src}: a Str result is the next hook’s line and, last, the line
line_run carries; (), an exit, a fault or a wrong-shaped value
(plugins) leaves it. After, fire runs chpwd
with {old, new} if the cwd reading
differs from the one taken before the line — the session is the working
directory’s outermost handler, so a cd a within [dir:] undid fires
nothing — then post-exec with
{src, status}, src being the line that ran — for every line, a static
failure’s status included. An Err(Severed) prints the cause and ends the
session.
It matches the one flat Report:
Static— a parse, type, or host failure that never reached evaluation; itsrenderedis the whole caret report, printed verbatim.Ran— a run that compiled, matched on itsEnding:Settledprints viaprint_result;Raised,Stopped, andUnreturnable(a settled value the wire cannot carry) print the diagnostic already rendered at the transport seam, verbatim (a closed-stdout stop renders to nothing);Exited(code)ends the loop (clamped throughplatform::exit_byte).Stoppedis a cancellation, not a process stop: ral does not suspend (ral-does-not-suspend).
The same door backs every host program the REPL holds as a value: the prompt
body, an rc startup block, and a plugin hook are registered hooks run as
Program::Hook runs (plugins), whose registered policy
decides capture, terminal authority, and whether the run is taken aside. A
hook run is no ready boundary, and its fault comes back labelled with the
hook’s name (plugin 'p' hook 'h', prompt) by the engine; an exit from a
hook is ignored.
The host
host.rs::ReplHost is the REPL’s Host, and the one door every hook
dispatch goes through (dispatch, run_hook → HookResult {outcome, captured, ctx}, the Outcome one of Settled(value), Exited,
Faulted(text), Walled(text), the text trimmed and labelled once there).
enquireanswers the three classesenquiry.rstypes once for both ends:`repl-editor(anEditorOp—get,set,set-lbuffer,insert,push,accept,ghost,highlight,history,state-get,state-set— applied to the editor context installed for the dispatch in flight, refused in its own words when none is),`repl-plugin(loadedwith a manifest,unloadedwith a name — told to the plugin runtime, which may refuse) and`repl-clipboard(aClipboardReq, copied by the host).surfaceand theDeferredSinkimpl render what the engine surfaces: a`watchline (ral/src/surface.rs, shared with batch) through the frontend’s printer above the prompt, a`noticedim, an`observedobservation skipped by choice (the ral front-ends render no audit), and anything else dropped with a note naming its class.prompt_faultprints a render’s faults, joined, once while they repeat.
The selectable frontend
The loop drives a boxed Frontend, chosen after boot from the rc’s
Surface:
Surface::Minimal— the canonical-stdin editor.Surface::Readline(the default) — the rustyline editor.
create_frontend resolves it: the capability gate forces the minimal editor
on a dumb terminal whatever was asked, otherwise the surface preference
decides. The preference is set by the --surface flag (CLI wins)
or the rc surface: key. The two implementations, the Frontend trait,
and completion live in
frontend.
Prompt, rc, theme
prompt.rs—renderdispatches the registeredSession/"prompt"hook, registered with capture: its return value is the prompt, a returned unit falls back to its captured stdout. The prompt body readsuserandcwd, the ambient builtins, directly. Pluginprompthooks fold over the result, each a dispatch registered with capture and read through the sameprompt_text; the terminal title is written separately. A failing prompt — one whose value cannot cross included — falls back to the default❯(a plugin’s, to the prompt so far); the faults of one render print together throughprompt_fault, when they differ from the last render’s, and the session survives so the user can rebind it.config.rs— the rc’s eleven keys (env,prompt— registered as theSession/"prompt"hook —bindings,aliases,edit-mode,bell,surface,recursion-limit,plugins,startup,theme) are declared once, inForm::Rc’s table, andRcPlan::decodejudges them all engine-side, inside the boot door, beforeRcPlan::applycommits to the shell and theRcSettingsit answers; a refused rc has changed nothing. The first-run skeleton is written withcreate_new, so a concurrent rc is never truncated. A wrong type at one of the scalar keys is the table’s static failure, above;decodecatches those keys’ further shape rules (e.g.edit-modemust be'emacs'/'vi', not just aString).theme.rs—OutputTheme(thevalue-prefix, default"=> ", and an optionalvalue-color, default yellow) governs value rendering; held bySessionand passed tostep; an unknown theme key refuses the rc.errfmt.rs— the REPL-styled plugin notices (the breaker’s disable notice, a plugin warning), beside core’s full ariadne renderer, andreport_failed_load, the one printer for a failed rc, profile, or plugin load: the report, then aral: WHAT: …line saying how far it got.cursor.rs— the zsh-style partial-line marker, written before each rustyline prompt. On Unix it isPROMPT_SP’s fill-and-erase —%, thenwidth − 1spaces, then\r\x1b[K— which leaves the%only when the fill wrapped off a partial line, and asks the terminal nothing, so keys typed while a command ran reach the editor intact. On Windows the console API, which likewise reads no input, reports the cursor column directly.