Flash Architecture
- Snapshot
- v0.2.0
- Source path
components/flash/docs/architecture.md- Tree
c41b976e0362
This guide is for maintainers working on Flash internals. It follows source code through parsing, analysis, planning, pipeline execution, platform calls, interactive front ends, and process cleanup. For language usage, see the Language Guide. Build and test procedures are in Development.
Project status: FlashOS as a complete operating system remains pre-alpha software. Flash 1.0.0 is released as the component contract in the current source. This guide describes its architecture and implementation boundaries. Availability in a particular FlashOS image or on another target is qualified separately; execution on a Linux or macOS host is not proof of FlashOS target support.
On this page
- Architectural scope
- Design principles
- Workspace and dependency direction
- Source and syntax front end
- Formatter launcher frontend
- Execution-plan inspection frontend
- Modules and static analysis
- Shared tooling services
- Language-server protocol adapter
- Runtime and session state
- Command resolution and execution planning
- Pipeline execution
- Platform capability boundary
- Interactive front end
- Jobs and process lifecycle
- Diagnostics and failure containment
- Safety and portability boundaries
- FlashOS integration
- Sources of truth
Architectural scope
Flash is a userspace command-language implementation. It owns parsing, evaluation, structured values, internal commands, process orchestration, interactive session behavior, and the fsh executable.
It does not own:
- the operating-system kernel or ABI;
- authentication and login services;
- the filesystem implementation;
- external userspace commands;
- image construction or package management;
- terminal and process primitives supplied by the operating system.
The architecture therefore separates language semantics from operating-system access. Most of the implementation can be exercised without starting a process or opening a terminal, while target-specific behavior remains behind explicit interfaces.
The main execution path is:
- 01Source text
- 02Source model, lexer, parser, and diagnostics
- 03Syntax tree
- 04Session and evaluator
- 05Command expansion and resolution
- 06Inspectable execution plan
- 07Preflight validation
- 08Internal, external, or mixed pipeline executor
- 09Platform capability interface
- 10Operating-system processes, files, pipes, signals, and terminal
Design principles
One language front end
Scripts, interactive submissions, formatting, input classification, and editor services use the same syntax crate. Flash does not maintain a separate interactive grammar or translate source into another shell language.
Planning before execution
Command words, spreads, closures, redirection targets, command resolution, pipeline carriers, and session options are captured in an execution plan before process execution begins.
Planning does not spawn a process or apply a redirection. A separate preflight step rejects invalid carrier edges, descriptor conflicts, unrepresentable native values, and other plan-level errors before platform resources are created.
Explicit data boundaries
Internal stages exchange one of four pipeline carriers:
- no payload;
- a byte stream;
- one structured value;
- a stream of structured values.
The runtime does not implicitly display, serialize, decode, encode, collect, wrap, or flatten one carrier into another. External processes remain byte-oriented, and explicit converter commands cross the structured-data boundary.
Direct external execution
External commands receive an executable path, native argument vector, environment snapshot, working directory, descriptor map, and process-group placement. Flash does not render the command back into source text or route it through /bin/sh.
Capability-based platform access
The runtime requests named capabilities rather than assuming that every target provides POSIX process groups, signals, terminal ownership, directory enumeration, or configuration directories.
A missing capability produces a specific diagnostic. It is not silently emulated with weaker behavior.
Bounded materialization
Streams remain lazy wherever the operation permits it. Operations that must retain command output, collect a stream, parse a complete document, or format buffered data enforce explicit resource limits rather than treating available memory as an unbounded contract.
Workspace and dependency direction
Flash is a nested Cargo workspace rooted at components/flash/. The workspace manifest is authoritative for current package membership. The architecture is defined by responsibilities and dependency direction rather than by a permanent number of crates.
| Responsibility | Current owner |
|---|---|
| Source representation, spans, lexical analysis, parsing, syntax trees, formatting, and source diagnostics | flash-syntax |
| Values, scopes, evaluation, functions, command metadata, planning, pipelines, sessions, jobs, module analysis, and shared semantic services | flash-runtime and syntax-owned analysis interfaces |
| Portable operating-system capability contracts and deterministic test adapters | flash-platform |
| Unix-like process, descriptor, filesystem, signal, and terminal integration | flash-platform-posix |
| FlashOS target policy, route composition, and qualification-gated adapter | flash-platform-flashos |
Command-line modes, interactive front ends, configuration, history, tooling entry points, and fsh assembly |
flash-cli |
Versioned document overlays, JSON-RPC/LSP projection, stdio framing, and flash-language-server assembly |
flash-lsp |
Portable language semantics depend on syntax and abstract platform contracts,
not on a concrete operating-system adapter. Concrete adapters depend on the
abstract capability interface. flash-cli selects the appropriate runtime
adapter and user-facing services. flash-lsp instead depends directly on
flash-syntax and flash-runtime; it has no dependency on flash-cli,
flash-platform-posix, or terminal integration and declares no direct
dependency on flash-platform.
New crates may be introduced as implementation responsibilities grow, but they must preserve this dependency direction and must not create an alternative grammar, evaluator, name resolver, or platform contract.
Source and syntax front end
The flash-syntax crate is the sole owner of Flash source structure.
Its public model includes:
SourceFileand stable source identities;- byte-based
Spanvalues and line-column conversion; - lexical tokens and invalid-token classifications;
- parser output and incomplete-input classifications;
- syntax-tree nodes;
- structured diagnostics;
- canonical source formatting.
Source ownership and spans
A source file owns its name and text. Syntax nodes retain spans into that source rather than copying arbitrary fragments of source text.
The same spans flow into later runtime stages. Expansion, command planning, redirection handling, carrier validation, process failures, and structured-data errors can therefore attach diagnostics to the source construct that caused the failure.
Paths and external arguments may contain native non-UTF-8 units, but Flash source itself remains UTF-8. The source model and the platform-native data model are deliberately separate.
Parsing outcomes
Parsing distinguishes three outcomes:
Complete
Incomplete
Invalid
A complete result contains a syntax tree. An incomplete result identifies source that may become valid with more input, allowing an interactive editor to request continuation without inventing its own delimiter logic. An invalid result contains diagnostics for source that should be reported immediately.
This distinction is reused by interactive validation rather than approximated with terminal-editor heuristics.
Formatter launcher frontend
fsh format --check and fsh format --write are non-executing launcher modes
owned by flash-cli. Their pure argument classifier retains ordered native
paths, and their formatter orchestration receives only an injected filesystem
capability. It does not construct a module program, runtime session, platform
adapter, executable probe, terminal, configuration loader, or history store.
The orchestration assigns stable source identities in operand order, decodes
each explicit file as UTF-8, and delegates canonical text exclusively to
flash_syntax::format_source. Check mode turns the first scalar divergence into
an anchored FMT001; incomplete and invalid input retain shared syntax
diagnostics. Imports are ordinary retained syntax and are never traversed by
the formatter frontend.
Both operations inspect and prepare the complete ordered batch. Write mode performs no replacement after any preflight failure. Its host adapter rejects final symlinks and nonregular or canonically duplicate targets, rechecks source bytes and permission bits, and replaces each changed file through a unique same-directory temporary file using complete write, file synchronization, and atomic rename. The adapter preserves permission bits only and deliberately does not present ordered per-file replacement as a multi-file transaction.
Execution-plan inspection frontend
fsh plan [--] SOURCE exposes the runtime's concrete planning boundary for one
top-level foreground command pipeline. The CLI frontend first uses the shared
module parser and static command analysis, then supplies a fresh lexical scope,
the inherited cwd and environment, default session options, the standard
command registry, and a read-only executable probe to the ordinary planner. It
runs structural preflight before rendering the plan.
The frontend's capability surface contains source canonicalization/loading and
executable metadata probing, but no runtime platform, session, editor,
configuration, history, writable filesystem, terminal, or process interface.
Command substitution and broader script shapes are rejected rather than
approximated. ExecutionPlan::render is the single deterministic rendering
owner and preserves source order, retained spans, and escaped native bytes; its
output is inspection text rather than source, serialization, or a launcher
protocol.
Shared editor services
Syntax highlighting, completion context, formatter behavior, and multiline validation are built from Flash tokens, spans, parse outcomes, and command metadata.
These services may present different user interfaces on different targets, but they do not define alternative language semantics.
Modules and static analysis
Multi-file programs are represented as a graph of canonically identified source modules. Module resolution records the importing source, the requested path, the canonical module identity, and the source spans required for diagnostics.
The shared syntax tree represents a static dependency as a top-level import '<path>' declaration. The path is a nonempty exact literal rather than an expression, so module discovery never depends on evaluation, interpolation, environment state, or globbing.
Canonicalization and source-byte loading are separate injected capabilities. The
recursive program analyzer assigns stable source identities in first-visit
depth-first order, decodes and parses each canonical module once, retains alias
imports as distinct graph edges, and registers both canonical module and source
identities for later diagnostics. It registers a module before traversing its
imports, allowing the graph to reject a back edge without unbounded recursion.
Report-oriented discovery retains decoded sources and continues through
independent sibling failures. Name analysis begins only with a complete graph,
and signature analysis begins only with clean names; poisoned invalid owners
suppress dependent cascades. A complete ModuleProgram is produced only when
every error-classified phase succeeds, so execution receives no partial graph
or registry.
Loading is analysis, not execution. The module program contains the canonical graph, source files, parsed syntax, deterministic local/export/import/reference tables, a resolved type registry, and direct/transitive initializer-effect summaries. Explicit export { name } lists can expose top-level lexical declarations and functions; import { name } from '<path>' resolves only names explicitly exported by the canonical target. Every loaded source is then resolved in evaluator-matched source order, including sources reached only through load-only imports. Each reference retains its complete source span and local declaration target; an imported reference also retains the import identifier and the target declaration/export provenance. The type registry retains source-spanned annotations and named-function signatures, including normalized attached documentation, for exact-span lookup. Unknown, private, duplicate, colliding, or invalid type names fail analysis with source-anchored diagnostics, and there is no wildcard import path.
The root module begins above an implicit immutable args: List[String] input
containing the ordered UTF-8 operands supplied after the script path. This
synthetic parent scope permits ordinary root source-order shadowing without
inventing a declaration span. Dependency modules do not receive the input.
Non-interactive fsh <script> execution injects the host filesystem adapter at
this boundary and reports grouped excerpts for diagnostics that span multiple
registered sources. The runtime derives deterministic source-edge depth-first
postorder from the analyzed named-import tables, initializes each canonical
dependency once, and executes the root last. A source reached only through
load-only imports remains dormant. The execution loader selects the
first deterministic analysis error, while the checker frontend retains all
independent issues allowed by the phase barriers; both consume the same
successful program representation.
Each activated module executes through the existing session driver with an isolated lexical root. Completed exports are cloned into importer roots as immutable snapshots; private bindings never become ambient names. Working directory, child-process environment, status, output, process activity, and background jobs remain shared across the whole program. Runtime binding cells and callables receive the resolved annotations owned by the program. Named callables also retain their signature-derived inspection metadata and defining source; imported callables therefore keep both correct help ownership and correct cross-file body diagnostics. Initializers, assignments, callable arguments, and named-function results use exact value-family checks. Static assignment analysis rejects read-only, imported-snapshot, captured, unknown, and conservatively known type-incompatible targets before execution.
The session driver commits successful statement-local cwd, environment, and
status changes before the next statement. Normal completion and explicit
whole-program exit copy the final child environment to the caller; runtime or
required-output failure does not. Output, filesystem operations, and process
activity cross their boundaries immediately and are never rolled back. One
program-owned job coordinator joins background work on normal completion,
explicit exit, runtime failure, and output failure, retaining ordered
background evidence and its existing completion precedence.
Host-free effect analysis walks the same syntax and command manifest without expansion, executable probing, or platform access. It records source-spanned working-directory, child-environment, status, output, filesystem-read, filesystem-write, process, job, program-exit, and opaque-external occurrences. Known callable bodies fold into the caller summary; indirect calls and external execution remain conservative. A transitive summary visits named dependencies once in runtime initialization order and excludes load-only edges. The model is descriptive and does not add a checker warning or a second execution policy.
The analysis layer is responsible for:
- resolving explicit imports and exports;
- constructing the module graph;
- rejecting import cycles with source-anchored diagnostics;
- resolving local, imported, exported, and private names;
- resolving source annotations and named-function signatures;
- checking known local and imported call arity and conservatively known types;
- retaining runtime contracts for dynamic binding and callable checks;
- classifying direct and named-dependency-folded initializer effects;
- collecting command metadata;
- validating typed pipeline connections;
- exposing the same results to the checker, help system, and language server.
Analysis must not depend on executing user code to discover names or signatures. It produces inspectable program information that execution can consume after validation.
Shared tooling services
The formatter, fsh check, help output, interactive editor features, and language server use the same source model, parser, syntax tree, module graph, name resolution, function metadata, initializer-effect summaries, and diagnostic types.
No tooling frontend may maintain a second Flash grammar or a competing name resolver. A language change is implemented in the shared language services first and then exposed through the relevant CLI, editor, and protocol adapters.
Static pipeline checking also shares its source-independent carrier contracts and
fault classification with runtime preflight. It walks retained syntax without
expanding words: exact built-ins use the standard command registry, forced and
assumed externals are byte-stream stages, and dynamic heads remain unknown.
Executable availability and PATH contents are not analysis inputs.
flash-cli exposes checking through a read-only adapter whose entire capability
surface is canonicalization and finite regular-file source loading. It does not
construct a runtime session, environment, executable probe, platform, terminal,
configuration loader, or history store. Execution remains a separate stage.
Formatting, checking, help lookup, completion, navigation, and language-server
requests must not start external commands or mutate the active shell session
merely to obtain analysis results.
Language-server protocol adapter
flash-language-server is a separate executable from fsh. It speaks JSON-RPC
2.0 over stdin and stdout using LSP Content-Length framing. Stdout is reserved
exclusively for framed protocol messages; the process does not inherit the
shell launcher, prompt, configuration, history, terminal, or process-reporting
paths. The protocol surface implements the stable LSP 3.17 core and exposes
only the capabilities it advertises during initialization.
The implemented surface is:
| Area | Methods and notifications |
|---|---|
| Lifecycle | initialize, initialized, shutdown, exit, and $/cancelRequest |
| Synchronization | textDocument/didOpen, textDocument/didChange, and textDocument/didClose |
| Diagnostics | textDocument/publishDiagnostics server notifications |
| Discovery | textDocument/completion, textDocument/hover, and textDocument/signatureHelp |
| Navigation | textDocument/definition and textDocument/references |
| Formatting | textDocument/formatting |
Synchronization is full-text with open and close notifications. Incremental
edits are not advertised. The server negotiates UTF-8 positions when the client
offers them and otherwise uses the protocol-default UTF-16 encoding; checked
conversion remains owned by flash-syntax. Only absolute file: document URIs
are accepted. TCP, sockets, batches, dynamic registration, workspace or watched
file operations, custom methods, progress, partial results, rename, code
actions, semantic tokens, and execute-command requests are outside this
surface.
Each open document owns an exact client version and an overlay over the read-only host loader. Unsaved roots and imported documents therefore enter the same canonical module graph as disk sources. One canonical module has at most one active overlay owner. Accepted open, full-change, close, or identity transitions advance the workspace generation and conservatively invalidate all open roots. Analysis jobs use immutable snapshots, merge diagnostics in deterministic canonical-root order, attach current open-document versions, and publish empty replacements for sources that leave the result set.
A reader, receive-order coordinator, sole writer, and one bounded worker keep
document changes and cancellation responsive while preserving deterministic
request order. Explicitly cancelled requests return RequestCancelled;
completed requests from an older generation return ContentModified; stale
diagnostics are discarded. Every request receives exactly one response.
Completion, hover, signature help, definition, references, and formatting are
projections of shared syntax, command metadata, and complete current module
programs. The adapter never initializes modules, evaluates expressions, expands
words, probes PATH or executables, opens redirections, mutates cwd or the
environment, accesses a terminal or runtime session, loads shell configuration
or history, or starts a process. Whole-document formatting delegates to the
shared canonical formatter and returns at most one edit.
Runtime and session state
The flash-runtime crate owns language evaluation and execution coordination.
A long-lived Session retains:
- the lexical
ScopeStack; - the logical working directory;
- the child-process environment;
- session options;
- the standard command registry;
- the most recent normally completed status;
- background-job state when job control is enabled.
Interactive submissions and complete script files both use this session driver. Script execution creates a session, submits the source, joins the jobs started by that script, and returns its structured completion or failure to the CLI.
Embedding boundary
Embedders can use flash-syntax for parsing and formatting and the shared
runtime analysis services without constructing the CLI. The pure evaluator
entry points deliberately have no filesystem, process, command-capture,
module-loading, or background-job host; they reject source that reaches those
effects instead of acquiring ambient operating-system access.
Full Flash programs use the runtime's ModuleProgramLoader plus
execute_module_program, or execute_script for one source without imported
modules. The embedding caller supplies module canonicalization and loading,
the command registry, executable probe, platform, clock, output sink, working
directory, and environment. A persistent interactive integration uses
Session and explicitly opts into job control. CLI configuration, history,
terminal editing, status-to-process-code mapping, and language-server transport
remain adapter responsibilities and are not installed implicitly by the
runtime APIs.
Runtime and CLI reporting boundary
flash-runtime owns language execution, cleanup, and outcome precedence. Its
script, module-program, isolated-chain, and interactive submission paths accept
an injected program-output sink. Script completion returns the optional final
Status plus ordered background-job reports; script failure returns the primary
structured error plus those reports. The runtime does not select host process
exit codes or write directly to process stdout or stderr.
flash-cli owns the executable boundary. Its host-free report layer maps a
classified completed Status to an exact eight-bit process code, writes program
bytes to stdout, and writes diagnostics and shell reports to stderr. Required
writes and flushes are checked. A failed program-output operation becomes
failure 1 and is reported on stderr when possible; a failed diagnostic stream
is not used recursively to report itself. Classification uses structured
outcomes and never parses rendered diagnostic text.
Normally completed codes 0..=255 remain exact. Numeric signals 1..=127 map
to 128 + signal; an unrepresentable status becomes diagnosed failure 1 rather
than being wrapped or truncated. Launcher misuse remains status 2, and other
shell-owned failures use status 1. Codes 1 and 2 therefore remain distinguishable
ordinary program results when they arrive through a completed Status.
One shared runtime diagnostic builder retains the primary source and ordered
call frames for ordinary and multi-source module failures. The CLI renders that
structured diagnostic once; it does not add a second summary or infer the
failure kind from the stable RUN001 heading.
Lexical state and process state
Lexical bindings and child-process environment entries remain separate:
The runtime may copy an environment entry into a lexical seed for an isolated child-shell execution path, but ordinary lexical declarations are not automatically exported.
Environment mutation is session-local until a child is planned. export
encodes exactly one eligible scalar and rejects NUL at the value's source span;
plan preflight applies the same rejection to inherited or embedding-supplied
snapshots. External spawn clears the host environment and installs only that
validated complete snapshot, so host-only variables cannot leak around the
session boundary.
The logical working directory also belongs to the Flash session. External stages and platform-backed internal commands receive the session's directory explicitly rather than depending on process-global directory mutation throughout the runtime.
Pure evaluation and command execution
Statements are divided into two broad paths:
- expressions, declarations, assignments, functions, and pure control flow use the evaluator;
- command jobs use resolution, planning, and one of the pipeline executors.
This separation keeps value operations and lexical scope behavior independent from descriptor and process mechanics.
Statements commit in source order. A failing statement reports its diagnostic without manufacturing a command status, while already completed earlier statements retain their established session effects.
Runtime values and carriers
Structured values and pipeline carriers are related but distinct:
Runtime value
Null, Bool, Int, Float, String, Bytes, Path, Duration,
ByteSize, List, Record, Table, Range, Status, Error, Function, Closure
Pipeline carrier
Empty, ByteStream, Value, ValueStream
A Bytes value is finite data held as one value. A ByteStream is a lazy, single-consumer execution resource. Similarly, a List is not automatically treated as a ValueStream.
The concrete lazy stream implementations live in stream.rs. Pulling a stream yields an item or chunk, normal exhaustion, a source-spanned failure, or cancellation.
Command resolution and execution planning
The command path is divided into registry lookup, external resolution, planning, and preflight.
- 01Command syntax
- 02Word and spread expansion
- 03Internal-command registry lookup
- 04External executable resolutionWhen required
- 05ExecutionPlan
- 06Preflight
Command registry
The CommandRegistry owns one
validated, deterministically ordered namespace manifest. Each spelling is a
canonical core command, an alias targeting exactly one core command, or a
reserved name protected from implicit external fallback. Core entries own
signatures; aliases borrow the canonical signature rather than copying it; and
reserved entries own no executable signature.
A signature declares:
- which carriers the command accepts;
- which carrier it returns, or whether it passes the input carrier through;
- command flags exposed to editor services;
- a stable invocation spelling and normalized user-facing documentation.
Invocable entries also carry their language-major introduction and optional deprecation/replacement lifecycle metadata. Construction rejects duplicate or empty names, invalid lifecycle data, missing or non-core alias targets, alias chains, and invalid replacements. Runtime command execution does not use manifest order as an override mechanism.
One classification operation returns unknown, core, alias, or reserved state.
Runtime resolution, planning, static analysis, background classification,
help, completion, and which consume that operation rather than maintaining
copied command lists or inferring namespace class from signature lookup. An
alias classification retains both the source spelling and canonical executor
identity, so every executor dispatches through the core implementation while
diagnostics and inspection preserve the spelling the program used.
A non-forced UTF-8 bare name is classified before external lookup. Core and
alias entries resolve internally; a reserved entry fails before PATH; and an
unknown name continues to external resolution. Forced-external and native
non-UTF-8 names retain the external path. Static checking uses the same
classification without probing host executables and gives a reserved stage an
unknown carrier contract to suppress dependent pipeline cascades.
External resolution
External resolution produces a native executable path. It observes the session environment and working directory while preserving native path units.
The resolver does not return rendered shell source. Its result is either an internal command name or an external executable path.
Execution plans
An ExecutionPlan records:
- the working directory;
- the complete child environment;
- ordered stages;
- resolved internal or external stage identities;
- native external argument vectors;
- typed internal-command arguments;
- accepted and produced carriers;
- pipeline operators and their source spans;
- source-ordered redirections;
- snapshotted execution options;
- an optional immutable help selection for an inspection stage;
- process-group policy;
- the complete pipeline span.
Closures accepted by internal structured commands remain typed callable values in the plan. They are not converted into command-line strings.
Because plans are inspectable independently of execution, plan construction and plan validation can be tested without launching the planned processes.
Preflight
Preflight validates the complete plan before an executor creates its first pipe or child process.
Validation includes relevant invariants such as:
- compatible carrier edges;
- representable external arguments and environment values;
- valid descriptor ownership;
- supported redirection combinations;
- stage forms accepted by the selected executor.
Platform capabilities are then checked by the operation that requires them. A plan can therefore be structurally valid while a particular target reports that the required operating-system capability is unavailable.
Pipeline execution
The runtime selects among three execution shapes.
All-internal pipelines
The internal executor moves owned carriers directly between internal commands:
InternalPayload::Empty
InternalPayload::ByteStream
InternalPayload::Value
InternalPayload::ValueStream
There is no intermediate terminal rendering or serialization.
Lazy commands such as filtering, mapping, line decoding, directory enumeration, and format conversion return pull-driven streams. A downstream stage that stops early can therefore stop requesting upstream items instead of forcing full materialization.
Commands that need session state, such as directory changes or explicit exit handling, receive a controlled session-state interface rather than unrestricted ownership of the complete session.
All-external pipelines
The execute module creates operating-system pipes, builds each child's final descriptor map, applies source-ordered redirections, and starts external stages directly through the platform interface.
All required stages are started before the executor waits for completion. This prevents a producer from filling a pipe while its consumer has not yet started.
Parent-owned pipe endpoints are released as soon as their ownership has been transferred or is no longer needed. Waiting produces source-ordered process results, which the runtime converts into leaf and aggregate Status values.
Command substitution and other capture paths drain output concurrently with child execution. When a capture limit is exceeded, the executor stops retaining further bytes but continues draining and reaping the pipeline before returning the error.
The same host-representable capture limit crosses the typed supervisor transport for complex background jobs. Transport framing has its own finite size and collection ceilings, but does not silently narrow the configured byte limit.
Mixed pipelines
The mixed executor partitions a plan into maximal internal segments and individual external stages. There is no topology limit on how many times they alternate:
external === byte pipe === [internal segment] === byte pipe === external
=== byte pipe === [internal segment] === byte pipe === external
Complete preflight validates every carrier edge and descriptor route before the executor creates a pipe, opens a target, or starts a child. External-to-external edges remain ordinary operating-system pipes. Every edge touching an internal segment retains exactly the parent-owned endpoint that segment needs.
All external children start before internal draining begins. Nonterminal
internal segments drain on scoped workers; a final internal segment remains on
the session thread so its output sink does not acquire a cross-thread
requirement. Each segment creates, transforms, and destroys its own structured
payload. Only owned byte-descriptor endpoints and immutable plan data cross a
worker boundary, so lazy ByteStream and ValueStream pull closures remain
single-threaded.
A source-order sequencer admits internal stage preparation deterministically while already prepared segments drain concurrently through bounded pipes. Session built-ins update one pending state under that sequencer. Lazy closure environments remain segment-local and merge into the pending environment in segment source order only after the complete pipeline succeeds.
The coordinator owns one status slot per source stage. Worker completion order
cannot reorder the aggregate. A check immediately after an external stage
forwards bytes without waiting, then validates that exact predecessor after
child completion; an unsuccessful deferred check is the ordinary structured
runtime error and prevents state commit.
Success commits pending cwd, environment, closure deltas, and aggregate status once, after every segment drain, final output, child wait, deferred assertion, and status slot succeeds. Runtime, cancellation, output, or wait failure discards that in-memory transaction. Bytes already displayed, files already changed, and process effects remain observable because they are not transactional.
A language try adds an enclosing checkpoint across lexical scope and the
host-owned in-memory session state. When a runtime error reaches it, the catch
block starts from that pre-try checkpoint with one fresh immutable Error
binding. Successful try or catch work follows the ordinary commit rules.
Cancellation, explicit exit, stopped-job control, and fatal host failures bypass
this boundary. The same external-effect limit applies: a catch cannot undo
bytes, files, or process effects that have already occurred.
One shared failure controller owns every external child until all segment
workers quiesce. A genuine segment or output failure closes endpoints,
cooperatively cancels peers, terminates and waits children, and selects the
earliest source-stage failure rather than a scheduler-race winner. BrokenPipe
at an internal-to-external boundary remains ordinary downstream early
completion. Explicit exit stops later preparation, cleans up children, and
commits only source-prior admitted state before returning its requested code.
Pipeline assignment still precedes local redirection. A byte-producing segment tail either drains to its external successor or to its final resolved local file route; an overridden pipe writer is dropped so the successor observes EOF. Unsupported internal descriptor shapes fail preflight. A structured carrier cannot cross an external boundary directly and still requires an explicit codec or format command.
This architecture preserves:
- ordinary streaming for external programs;
- structured lazy evaluation between internal stages;
- bounded bridging at every repeated representation boundary;
- deterministic source-ordered preparation and aggregate status;
- transactional in-memory session state without false rollback claims for external effects; and
- exact child, endpoint, and terminal cleanup on every exit route.
Terminal presentation
Human-readable presentation is selected only for a final structured carrier in an interactive session whose output is an unredirected terminal.
Terminal tables and other display forms are presentation, not serialization. Redirected output and external consumers require an explicit byte-producing stage.
Platform capability boundary
The flash-platform crate defines the interface between portable runtime logic and operating-system operations.
The Platform trait is synchronous and blocking. Concurrency is arranged by the runtime with threads where required; Flash does not impose an asynchronous runtime on the CLI or target adapter.
Capability groups
The interface separates capabilities such as:
- environment and working-directory access;
- file actions and directory enumeration;
- anonymous pipes;
- direct process spawning and waiting;
- process groups;
- signal delivery;
- foreground terminal ownership;
- terminal information and raw mode;
- monotonic time;
- standard directory discovery;
- discovery of the running shell executable;
- process hang-up disposition.
A platform exposes the set it supports. Each capability method verifies the relevant capability before touching the host.
Error taxonomy
Platform failures remain distinguishable:
| Failure | Meaning |
|---|---|
| Unsupported capability | The adapter cannot provide the requested operation |
| Unavailable capability | The operation exists but cannot be used in the current session |
| Operation failure | A specific spawn, open, wait, signal, or descriptor operation failed |
This distinction allows the runtime to report a missing feature separately from a failed use of an available feature.
Byte-preserving types
Executable paths, arguments, environment values, and filesystem paths cross the boundary through native OsStr, OsString, Path, and PathBuf representations.
Portable language code therefore does not need to convert all operating-system data through UTF-8. A conversion to text occurs only where a language operation explicitly requires text.
Owned resources and guards
Platform resources use ownership rather than shared raw identifiers wherever possible:
- child-process handles own wait state;
- descriptor endpoints own pipes or files;
- directory streams own enumeration state;
- terminal-mode guards restore previous attributes;
- foreground-terminal guards return ownership to the shell;
- signal-arrangement guards restore prior dispositions.
Restoration is also performed when a guard is dropped, providing a cleanup boundary for early returns and propagated errors.
Adapter roles
A concrete adapter implements the abstract platform capability contract for one operating-system environment.
flash-platform-posix provides the Unix-like process, descriptor, filesystem, signal, and terminal routes composed by concrete adapters. Its behavior on Linux or macOS is host evidence, not automatic FlashOS qualification.
flash-platform-flashos is the dedicated FlashOS adapter. It composes the 41 classified existing Rust and relibc routes behind the portable contract and owns the shimmed standard-directory policy: absolute native HOME and XDG roots are preserved, while missing or relative values receive deterministic FlashOS home, configuration, cache, and state fallbacks. These target details do not enter flash-runtime.
Redox-target fsh selects the FlashOS adapter in interactive, script, and background-supervisor modes. The adapter advertises the image-qualified groups used by foreground execution, scripts, structured commands, pipelines, redirections, directories, and background waits. Signals remain absent because the target has not qualified the complete stopped/continued/signaled transition vocabulary; Flash reports that missing capability instead of silently weakening job control.
The runtime depends only on the abstract capability contract. It must not silently emulate a missing target capability with weaker POSIX behavior. Release and target evidence determine which adapter capabilities may be claimed publicly.
FlashOS target baseline
The machine-readable
flashos-x86_64.toml record identifies the
platform boundary that a FlashOS adapter targets. The current baseline records:
| Boundary | Current identity |
|---|---|
| Rust target | x86_64-unknown-redox |
| Rust target configuration | target_os="redox", target_env="relibc", 64-bit little-endian ELF |
| Target compiler | Redox Rust branch selector redox-2026-05-24, reporting rustc 1.98.0-dev, unknown source commit, and LLVM 21.1.2 |
| C runtime | relibc, libc.so.6, with /lib/ld64.so.1 as the executable interpreter |
| Flash executable | x86_64 position-independent ELF requiring libc.so.6 and libgcc_s.so.1 |
Candidate and release images cook their selected packages from tracked recipes.
The record binds the staged relibc package's source identity to the revision
selected by its recipe, preventing a moving binary feed from silently replacing
the configured userland input.
The compiler likewise reports its release and LLVM identity but no source
commit; the record preserves that limitation instead of turning the dated
branch selector into a revision claim.
Source validation keeps the record aligned with the image profile, source
package rule, build toolchain, Rust recipe, and relibc recipe. Image
qualification additionally checks Cargo's compiler fingerprint, the staged
relibc source identity, and the ELF headers of the staged fsh and C runtime.
These facts establish target identity only. They do not classify a platform
capability as supported or prove that a capability works at runtime.
The separate
flashos-x86_64-capability-evidence.toml
inventory compares every current Capability variant with the source path
selected by the Redox executable and with observations already made by the
FlashOS QEMU contract. It records requirements, source observations, runtime
observations, and explicit evidence gaps without turning them into support
classifications. In particular, the selected adapter's capability
declaration remains a claim under comparison; it is not accepted as target
qualification merely because the same code passes on Linux or macOS.
The paired
flashos-x86_64-operation-map.toml
then maps every requirement in that inventory to its current boundary. Some
operations remain entirely inside Flash, direct job-control and terminal calls
reach configured relibc ABI and Redox userland paths, and higher-level
environment, process, filesystem, directory, executable, and time calls stop at
public Rust standard-library APIs because the target compiler source commit is
unknown. Configuration-directory operations route through the dedicated
FlashOS policy shim. These are mapping facts, not native, adapted, unsupported,
or kernel-work classifications, and they do not replace target behavior
evidence.
The separate
flashos-x86_64-capability-classification.toml
consumes the complete map and classifies the implementation route for every
operation and capability. Existing Flash-internal, target standard-library,
and configured-relibc routes are native. Standard-directory discovery,
native-path preservation, and fallback policy are shimmed because FlashOS must
define and wire an explicit target convention over existing filesystem
primitives. No current operation is deliberately unsupported or requires
kernel work. An architectural route verdict is not a behavioral support claim.
The selected dedicated adapter enables the separately image-qualified groups;
the classification record remains distinct from those runtime observations.
The versioned
flashos-x86_64-capability-report-v1.toml
records the adapter's advertised set against the current Flash workspace and
FlashOS release versions. Every advertised group names at least one owning
fixture from
flashos-x86_64-runtime-fixtures-v1.toml,
while Signals remains withheld with no fixture claim. The fixture suite is
consumed by the QEMU runner and can be rendered as the same ordered checklist
for a manually observed real system.
The report's qualification level is deliberately bounded. It establishes a
durable relationship among the selected adapter, classified routes, reusable
smoke interactions, and known limitations. It does not claim exhaustive Flash
language/session/editor behavior, physical-hardware success, release
qualification, or the complete stopped/continued/signaled transition
vocabulary.
The report links the separate versioned
flashos-x86_64-target-matrix-v1.toml
contract. That matrix gives every advertised classified operation one owning
target case and covers the required startup, configuration, script, built-in,
argv/environment, directory, pipeline, redirection, cancellation, history,
completion, structured-data, capture, error, dynamic-command, status, glob,
Unicode/multiline editor, supported-job, and clean-exit surfaces. QEMU executes
the cases in order, and the same record renders an operator-observed target
checklist. Signals, physical hardware, and release qualification remain
outside this matrix.
Test adapters
The platform crate also provides deterministic fake and recording adapters.
They allow tests to:
- select supported and unsupported capabilities;
- script process results;
- inspect spawn requests;
- observe process-group placement;
- record signal delivery and terminal handovers;
- exercise cleanup behavior without using the host's real process table or terminal.
This keeps most runtime verification independent from the POSIX adapter and reserves host or pseudoterminal tests for behavior that genuinely crosses the platform boundary.
Interactive front end
The flash-cli crate combines the runtime and selected platform adapter into the fsh executable.
Its top-level modes are:
help
version
static checking
canonical formatting
script execution
interactive session
reserved internal child-shell execution
The reserved child-shell path supports background conditional chains and is not part of the public command-line interface.
Editor boundary
The interactive loop depends on the synchronous LineEditor boundary rather than on a specific terminal-editing library.
An editor produces events such as:
- submitted source;
- cancellation of the current edit;
- end of input.
The interactive loop owns the sequencing of prompts, notices, diagnostics, evaluation, and exit decisions. Evaluation receives an injected program-output sink, while the driver owns checked output and diagnostic flushes plus the final host result. Recoverable diagnostics return to the same session. Fatal editor, output, diagnostic, or platform failure performs unconditional session cleanup before returning status 1; if the diagnostic stream itself failed, the driver does not attempt another write through it. The editor owns terminal writes that must appear safely before the next prompt.
Host and target editors
macOS and Linux builds use the Reedline-backed adapter for parser-driven multiline validation, highlighting, completion, history, and hints.
The Redox path uses the Flash terminal editor when both input and output are terminals. A canonical line reader remains available as a fallback when raw terminal editing is unavailable or output is redirected.
These adapters share the same session evaluator, syntax implementation, and editor-neutral completion, highlighting, hint, history, external-print, and resize contracts. The portable editor implements Tab completion, parser-owned styles, history hints, persistent recall, grapheme-aware editing, display-cell rendering, whole-submission multiline movement, in-flight resize, and safe redraw around background notices. This source parity is host-tested; FlashOS runtime qualification remains required before the behavior is claimed for an assembled target image. Differences in editor facilities do not create different script semantics.
Configuration and history
Configuration and history selection occur before the interactive
Session is created. A successfully initialized configuration seeds the
session scope and environment plus typed pipefail and capture-limit options
and completion/history policy. Six config-only mutable bindings carry those
settings plus primary and continuation prompt strings through the isolated
transaction and are removed before the live lexical scope is installed. Safe
mode restores clean settings and its fixed [SAFE] >> prompt; config bypass
retains ordinary defaults. The CLI --no-history policy remains an
unconditional override.
The config loader opens one read-only close-on-exec handle and verifies the
type, effective-user ownership, and write permissions of that same object
before bounded UTF-8 decoding. Persistent history opens its final directory
with no-follow and close-on-exec, validates that directory handle, and opens
the history file relative to it with no-follow and close-on-exec. Each history
sync revalidates ownership and exact 0700/0600 modes on the handles it
actually uses, avoiding a pathname-check/open gap.
At each prompt boundary the interactive evaluator snapshots the live registry
and lexical scope, then collects executable names from the child PATH and
recursive path candidates from the logical cwd. The cancellable collector is
outside the keypress callback and is bounded to 256 directories and 4,096
entries per host candidate family. It does not follow directory symlinks and
omits native paths that have no exact UTF-8 source representation. Crossing a
ceiling discards that family for the prompt, preserving deterministic results
instead of retaining a host-ordered prefix. Each completed snapshot has a
monotonic generation, and the editor rejects a stale generation before it can
replace the current immutable engine.
Path replacement remains syntax-owned. The ordinary lossless token stream
provides quote mode, interpolation boundary, grammar role, and exact UTF-8 byte
span. Semantic matching consumes the same wildcard component rules as the
explicit glob(...) runtime, while rendering produces reversible bare or
quoted Flash source. Completion therefore observes filesystem candidates
without expanding an argument or evaluating the edit buffer.
The host and Redox executables select their native configuration and state-path conventions at compile time. The Redox source path now wires transactional configuration, completion policy, portable persistent history, prompts, and both opt-out flags into the portable editor. Development-host and forced-editor tests establish source parity, not availability inside a FlashOS image; target runtime qualification remains separate.
Prompt-safe notices
Background-job notices are transferred from the runtime as structured records with stable identities. An active editor clears its presentation, writes the complete notice, and redraws the same prompt, buffer, cursor, and completion state. Notices already pending at a prompt boundary use the same editor-owned write path. The runtime acknowledges a notice only after successful presentation.
This prevents asynchronous output from corrupting an active edit or losing a completion observed while the editor owns the terminal.
Jobs and process lifecycle
Job management is divided between execution mechanics and a session-owned coordinator.
Foreground pipelines
External members of one foreground pipeline are placed in one process group when the platform supports that capability. An interactive shell may hand the terminal to that group for the duration of the foreground wait.
Terminal ownership is held by a guard and restored before the next prompt. A failed restoration is reported rather than silently continuing with a shell that may no longer receive terminal input.
Background jobs
A background job receives a stable Flash job identity that is separate from operating-system process and process-group identifiers.
The coordinator owns:
- job identities;
- member process handles;
- process-group metadata;
- running, stopped, continued, and completed observations;
- aggregate statuses;
- addressable job state;
- prompt-safe notices;
- cleanup and reaping state.
Observer tasks report process transitions to the coordinator. The session remains the sole owner of the job table and applies those observations in a defined order.
Direct and supervised background execution
A background chain that consists of one all-external pipeline can use the direct pipeline launcher.
More general background conditional chains use a re-executed fsh child as their supervisor:
- 01Interactive or script sessionStarts one child shell
- 02Background supervisor processParses and executes the conditional chain
- 03Internal and external child work
The supervisor is the addressable job member. It plans the chain in the child process, reports the chain's aggregate status, and remains alive long enough to reap external descendants during shutdown.
The internal invocation is reserved so that this mechanism does not create a public command-string execution mode.
Lifetime policy
A script joins the background jobs it started before returning to its caller. Background failures can therefore affect the final script result rather than being silently orphaned.
An interactive session warns before leaving live jobs. A confirmed exit resumes stopped jobs where necessary, sends the session's hang-up request, and waits for completion.
Destructive termination is an explicit job operation rather than an automatic timeout escalation.
Diagnostics and failure containment
Flash keeps several outcome classes separate:
parse outcome
runtime value or control completion
normal command Status
runtime error
cancellation
platform or output failure
A nonzero external exit is a normal Status, not a runtime error. Operators such as && and || branch on Boolean or status success, while check explicitly converts an unsuccessful status into an evaluation error.
Source anchoring
Errors retain the most relevant source span available:
- invalid tokens and grammar failures point into the source front end;
- expansion failures point to the affected word or spread;
- command-resolution failures point to the command head;
- incompatible carrier errors point to the pipeline boundary;
- redirection failures point to the relevant operator or target;
- process failures point to the planned command;
- lazy structured errors point to the stage that owns the producer.
Runtime stack frames can add callable context without discarding the original span.
Failure precedence and cleanup
When execution has already acquired resources, cleanup still occurs before the error is returned.
Depending on the execution path, cleanup may include:
- closing parent descriptor owners;
- terminating and reaping already-started children;
- draining capture pipes;
- waiting for remaining pipeline stages;
- restoring foreground terminal ownership;
- restoring terminal attributes or signal dispositions;
- joining or hanging up session-owned background jobs.
Cleanup failures do not automatically replace the more informative originating failure. The relevant executor defines which outcome remains primary.
No false transactional claim
Preflight prevents many failures before execution begins, but process and filesystem execution is not a transaction.
For example, a source-ordered redirection may successfully create or truncate one file before a later file action fails. Flash cleans up owned resources, but it does not claim to reverse completed operating-system side effects.
Mixed execution does use one pending in-memory session transaction. Cwd, environment, closure deltas, and aggregate status become caller-visible only after final output, child waits, deferred checks, and status construction all succeed. That narrower transaction does not include bytes already written, filesystem changes, or process activity.
Safety and portability boundaries
Flash is implemented in Rust, but it is not accurate to describe the entire component as containing no unsafe code.
The crates containing language semantics and frontend orchestration prohibit unsafe code:
flash-syntax;flash-runtime;flash-platform;flash-cli.
The POSIX adapter denies unsafe code by default and permits it only in explicitly scoped implementation areas that require low-level system interfaces, including descriptor installation, process-group operations, terminal control, signal disposition, and child-status observation. Every unsafe block carries a local safety invariant, and workspace lints enforce that documentation across production, tests, and process fixtures while also requiring explicit blocks inside any unsafe function.
This arrangement creates a review boundary; it does not by itself establish the absence of defects.
Additional portability boundaries include:
- source text is UTF-8, while native paths and arguments may not be;
- the runtime does not assume every platform capability exists;
- terminal behavior is enabled only when the relevant streams are terminals;
- process-group and signal behavior remains adapter-owned;
- human display is never treated as a wire protocol;
- successful host execution does not qualify the FlashOS target;
- target compilation does not replace image-level execution evidence.
FlashOS integration
Flash is integrated into the FlashOS system through the package recipe at recipes/terminal/flash/recipe.toml.
The recipe builds fsh from flash-cli and flash-language-server from
flash-lsp for the active target, then installs both into the image package.
The active x86_64 product profiles include that package and configure
/usr/bin/fsh as the login shell.
- 01Flash package recipe
- 02FlashOS image
- 03LoginStarts /usr/bin/fsh
The package recipe snapshots tracked and non-ignored files from the current
components/flash/ workspace. A clean image build therefore uses the Flash
tree from the exact outer FlashOS checkout without a self-referential recipe
SHA. Local uncommitted component files are included only when they are not
ignored, allowing pre-commit image testing while excluding generated targets.
System-level package selection, image assembly, boot flow, and login configuration remain documented in FlashOS Architecture. This document owns the internal architecture of the Flash component after its executable starts.
Sources of truth
Use the following files when evaluating or changing an architectural contract:
| Concern | Primary source |
|---|---|
| Workspace membership and shared metadata | Cargo.toml |
| Source, syntax trees, classification, and diagnostics | flash-syntax/src/lib.rs |
| Runtime module boundaries | flash-runtime/src/lib.rs |
| Session ownership and submitted-source flow | session.rs |
| Script execution and background joining | script.rs |
| Command signatures and registry | command.rs |
| Command planning and preflight | plan.rs |
| External and mixed execution | execute.rs |
| Internal structured execution | internal.rs |
| Lazy byte and value streams | stream.rs |
| Background-job coordination | background.rs |
| Job identities and states | job.rs |
| Platform capabilities and test adapters | flash-platform/src/lib.rs |
| Concrete Unix-like platform operations | flash-platform-posix/src/lib.rs |
| FlashOS adapter and standard-directory policy | flash-platform-flashos/src/lib.rs |
| CLI assembly and target selection | flash-cli/src/main.rs |
| Interactive editor contract | editor.rs |
| Interactive control loop | interactive.rs |
| FlashOS package construction | recipe.toml |
| FlashOS product integration | FlashOS Architecture |
When descriptive documentation and executable behavior disagree, inspect the current source, manifests, tests, package recipe, and target evidence before changing the public architectural claim.
Related documentation
- Language Guide — Language syntax, values, bindings, expressions, commands, and structured-data semantics.
- Scripting — Script execution, external processes, redirections, statuses, and job control.
- Development — Workspace builds, tests, linting, fuzzing, fixtures, and local API documentation.
- Flash overview — Component purpose, public boundaries, and documentation entry point.
- FlashOS Architecture — System layers, image construction, package integration, and boot-to-shell flow.
- FlashOS Verification — Evidence boundaries between host checks, target builds, images, QEMU, and hardware.