Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/adr/0007-engine-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 0007: Engine tree

xs runs Nushell in two places: the one-shot evaluator and the actor, service, and action processors. Both build their engine from `nu::prepared_base`, a base engine plus the store commands, cloned per spawn.

## Prepare once, clone per use

A subsystem that builds engines repeatedly prepares its base once and clones it per spawn, rather than rebuilding from scratch each time.

## The base

`prepared_base` starts from a base engine and adds the store commands (core, read, and the per-runner write). The base defaults to `Engine::new()`: the nu context, the standard library, and the environment.

## An embedder can supply the base

A program that embeds xs can supply the base via `Store::with_base_engine`, so its own commands reach the processors. `prepared_base` clones the supplied base per spawn and adds the store commands on top; with no base set it uses `Engine::new()`.

The `prepared_base_carries_store_base_engine_commands` test stands in for an embedder: it puts an `xs-base-marker` command on the base and asserts a processor engine resolves it.

```mermaid
flowchart TD
Base["embedder base via with_base_engine: Engine::new() + xs-base-marker"]
Base --> Eval["one-shot eval: xs-base-marker"]
Base --> Proc["actor / service / action processors: xs-base-marker + store commands"]
```

## Relation to ADR 0006

[ADR 0006](0006-store-builtins-on-prepared-engine.md) established the prepared, cloneable base and the store-command layering. This ADR lets an embedder supply that base's non-store portion.
9 changes: 8 additions & 1 deletion src/nu/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,14 @@ pub fn add_write_commands(
/// each clone. Actors pass `direct_write: false` and add their per-instance
/// buffered `.append` to the clone.
pub fn prepared_base(store: &Store, read: ReadMode, direct_write: bool) -> Result<Engine, Error> {
let mut engine = Engine::new()?;
// Clone the embedder's base engine when the store carries one, else build
// the default. See ADR 0007.
let mut engine = match store.base_engine() {
Some(base) => Engine {
state: base.clone(),
},
None => Engine::new()?,
};
add_core_commands(&mut engine, store)?;
engine.add_alias(".rm", ".remove")?;
add_read_commands(&mut engine, store, read)?;
Expand Down
2 changes: 2 additions & 0 deletions src/nu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub use engine::{
pub use util::{frame_to_pipeline, frame_to_value, value_to_json};
pub use vfs::load_modules;

#[cfg(test)]
mod test_base_engine;
#[cfg(test)]
mod test_commands;
#[cfg(test)]
Expand Down
75 changes: 75 additions & 0 deletions src/nu/test_base_engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use tempfile::TempDir;

use nu_protocol::engine::{Call, Command, EngineState, Stack};
use nu_protocol::{Category, PipelineData, ShellError, Signature};

use crate::nu::{prepared_base, Engine, ReadMode};
use crate::store::Store;

/// A trivial marker command standing in for a consumer-registered command like
/// http-nu's `.mj`. It must survive from a store's base engine into the engine
/// a processor builds via `prepared_base`.
#[derive(Clone)]
struct MarkerCommand;

impl Command for MarkerCommand {
fn name(&self) -> &str {
"xs-base-marker"
}

fn signature(&self) -> Signature {
Signature::build("xs-base-marker").category(Category::Custom("test".into()))
}

fn description(&self) -> &str {
"test marker command carried on a base engine"
}

fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(PipelineData::empty())
}
}

/// A command an embedder puts on the store's base engine must resolve in the
/// engine a processor builds via `prepared_base`.
#[test]
fn prepared_base_carries_store_base_engine_commands() {
let temp = TempDir::new().unwrap();

let mut base = Engine::new().unwrap();
base.add_commands(vec![Box::new(MarkerCommand)]).unwrap();

let store = Store::new(temp.path().to_path_buf())
.unwrap()
.with_base_engine(base.state);

let engine = prepared_base(&store, ReadMode::Stream, true).unwrap();
assert!(
engine
.eval(PipelineData::empty(), "xs-base-marker".to_string())
.is_ok(),
"a command from the store's base engine should resolve in a processor engine"
);
}

/// With no base set, `prepared_base` still yields a usable engine (the default
/// base plus the store builtins).
#[test]
fn prepared_base_without_base_engine_still_builds() {
let temp = TempDir::new().unwrap();
let store = Store::new(temp.path().to_path_buf()).unwrap();

let engine = prepared_base(&store, ReadMode::Stream, true).unwrap();
assert!(
engine
.eval(PipelineData::empty(), "echo hi".to_string())
.is_ok(),
"default prepared_base should still produce a usable engine"
);
}
24 changes: 24 additions & 0 deletions src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};

use std::sync::{Arc, Mutex};

use nu_protocol::engine::EngineState;
use scru128::Scru128Id;

use serde::{Deserialize, Deserializer, Serialize};
Expand Down Expand Up @@ -459,6 +460,10 @@ pub struct Store {
broadcast_tx: broadcast::Sender<Frame>,
gc_tx: UnboundedSender<GCTask>,
append_lock: Arc<Mutex<()>>,
/// Optional base engine the processors clone, set via [`with_base_engine`].
/// `None` falls back to `Engine::new()`. See
/// [`prepared_base`](crate::nu::prepared_base) and ADR 0007.
base_engine: Option<Arc<EngineState>>,
}

impl Store {
Expand Down Expand Up @@ -519,6 +524,7 @@ impl Store {
broadcast_tx,
gc_tx,
append_lock: Arc::new(Mutex::new(())),
base_engine: None,
};

// Spawn gc worker thread
Expand All @@ -527,6 +533,24 @@ impl Store {
Ok(store)
}

/// Set a base engine the processor engines clone.
///
/// A program that embeds xs prepares an [`EngineState`] with its own
/// commands, env, and consts;
/// [`prepared_base`](crate::nu::prepared_base) clones it per spawn and adds
/// the store commands, so the actor, service, and action processors get
/// those commands. Shared across `Store` clones, so set it once, before the
/// processors spawn. No base set: `Engine::new()`.
pub fn with_base_engine(mut self, base: EngineState) -> Self {
self.base_engine = Some(Arc::new(base));
self
}

/// The base engine an embedder attached via [`with_base_engine`], if any.
pub fn base_engine(&self) -> Option<&EngineState> {
self.base_engine.as_deref()
}

/// Wait until the background garbage-collection worker has processed every
/// task queued so far. Useful in tests to observe TTL eviction
/// deterministically.
Expand Down
Loading