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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ a + b

**Execution context** — Rib source is **not** compiled into the guest component. In a REPL integration, Rib text is entered at an interactive prompt; it is parsed and interpreted **in the host process** (the binary that embeds the runtime, e.g. the Wasmtime CLI). That host then performs the actual **component calls** according to the embedder’s `invoke` implementation.

**Instance identity** — Each bare `instance()` in Rib is a **new logical worker** (independent guest state). Passing the same string to `instance("…")` aliases the **same** worker. Integrators must map each worker name to **one** guest component instance for the session (see the [language guide](docs/language-guide.md#multiple-instance-calls-and-worker-identity) and [`rib-repl` README](rib-repl/README.md)).

---

## Repository layout
Expand Down
8 changes: 0 additions & 8 deletions docs/README.md

This file was deleted.

29 changes: 28 additions & 1 deletion docs/language-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Companion WIT — [`example.wit`](example.wit) exports `inventory` (records, enu
## Table of contents

0. [Example WIT (`example.wit`)](#0-example-wit-examplewit)
1. [`instance()` and calling exports](#1-instance-and-calling-exports)
1. [`instance()` and calling exports](#1-instance-and-calling-exports) (including [multiple calls and worker identity](#multiple-instance-calls-and-worker-identity))
2. [Programs, blocks, and semicolons](#2-programs-blocks-and-semicolons)
3. [Comments](#3-comments)
4. [Literals and Wave-shaped values](#4-literals-and-wave-shaped-values)
Expand Down Expand Up @@ -121,6 +121,33 @@ my-instance.lookup-sku(7)

That’s the whole pattern: one binding from `instance()`, then `that-name.export-name(…)`. Export names come from WIT and are usually kebab-case (`lookup-sku`, `format-stage`, …). Hyphens in `let` names are fine too (`store-main`, `lane-a`, …) if you prefer that style.

<a id="multiple-instance-calls-and-worker-identity"></a>

### Multiple instance() calls and instance identity

Each bare **`instance()`** (no argument) introduces a **new logical instance**: its own identity in the host, so **guest state is not shared** between different call sites. If a component keeps internal state behind an export (for example a counter), separate bindings behave like separate instances:

```console
>>> let x = instance()
()
>>> x.increment-and-get()
1
>>> x.increment-and-get()
2
>>> let y = instance()
()
>>> y.increment-and-get()
1
>>> y.increment-and-get()
2
```

Here `x` and `y` are independent: `y`’s counter starts again at `1`.

To **share** state across more than one Rib binding, give the **same worker name** as a string argument, e.g. `instance("my-worker")` for both bindings. The host maps that name to **one** live component instance for the session, so all calls using that name see the same state.

**Embedder contract** — Runtimes that integrate Rib (Wasmtime, Golem, tests, …) must follow that meaning: **worker name is the identity key.** For each distinct name, keep one guest `Instance` (or equivalent) for the session (unless you explicitly reload). Two different names ⇒ two isolated instances; the **same** string passed to `instance("…")` ⇒ **one** underlying instance. Each anonymous `instance()` must receive a **unique** generated name so their state never collides.

More calls against [`example.wit`](example.wit) → `inventory`:

```rust
Expand Down
3 changes: 2 additions & 1 deletion rib-lang/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// duplicate nested work (aggregate timers) are omitted from the table.

use std::cell::{Cell, RefCell};
use std::cmp::Reverse;
use std::io::{stderr, IsTerminal};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Once;
Expand Down Expand Up @@ -188,7 +189,7 @@ fn print_summary_table(wall: Duration, title: &str) {
return;
}

rows.sort_by(|a, b| b.1.cmp(&a.1));
rows.sort_by_key(|row| Reverse(row.1));

let s = Styles::detect();
let prefix = format!("{}[rib-profile]{} ", s.tag(), s.r());
Expand Down
21 changes: 10 additions & 11 deletions rib-lang/src/type_inference/global_variable_type_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,20 @@ fn override_type_arena(
for id in order {
let node = arena.expr(id);
match &node.kind {
ExprKind::Identifier { variable_id } => {
if variable_id == &spec.variable_id {
current_path.progress();
if spec.path.is_empty() {
types.set(id, spec.inferred_type.clone());
previous_id = None;
current_path = full_path.clone();
} else {
previous_id = Some(id);
}
} else {
ExprKind::Identifier { variable_id } if variable_id == &spec.variable_id => {
current_path.progress();
if spec.path.is_empty() {
types.set(id, spec.inferred_type.clone());
previous_id = None;
current_path = full_path.clone();
} else {
previous_id = Some(id);
}
}
ExprKind::Identifier { .. } => {
previous_id = None;
current_path = full_path.clone();
}
ExprKind::SelectField {
expr: inner_id,
field,
Expand Down
34 changes: 16 additions & 18 deletions rib-lang/src/type_inference/identify_instance_creation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,24 @@ fn search_for_invalid_instance_declarations_arena(
let node = arena.expr(id);
let span = node.source_span.clone();
match &node.kind.clone() {
ExprKind::Let { variable_id, .. } => {
if variable_id.name() == "instance" {
return Err(CustomError::new(
span,
"`instance` is a reserved keyword and cannot be used as a variable.",
ExprKind::Let { variable_id, .. } if variable_id.name() == "instance" => {
return Err(CustomError::new(
span,
"`instance` is a reserved keyword and cannot be used as a variable.",
)
.into());
}
ExprKind::Identifier { variable_id }
if variable_id.name() == "instance" && variable_id.is_global() =>
{
return Err(CustomError::new(span, "`instance` is a reserved keyword")
.with_help_message(
"use `instance()` instead of `instance` to create an ephemeral instance.",
)
.with_help_message(
"for a named instance, use `instance(\"foo\")` where `\"foo\"` is the instance name",
)
.into());
}
}
ExprKind::Identifier { variable_id } => {
if variable_id.name() == "instance" && variable_id.is_global() {
return Err(CustomError::new(span, "`instance` is a reserved keyword")
.with_help_message(
"use `instance()` instead of `instance` to create an ephemeral instance.",
)
.with_help_message(
"for a named instance, use `instance(\"foo\")` where `\"foo\"` is the instance name",
)
.into());
}
}
_ => {}
}
Expand Down
10 changes: 6 additions & 4 deletions rib-repl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ Example: Once the component is loaded, the following will result in `3`, if the

## Session semantics

**Single component instance** — The embedding keeps **one** guest `Instance` (or equivalent) alive for the session unless it explicitly reloads. Each evaluated line runs against **that** instance, so guest state, linear memory where applicable, and **resource handles** tied to the instance remain coherent across prompts. This matches common REPL expectations: stateful APIs can be exercised incrementally without reinstantiating on every line.
**One guest instance per Rib instance name** — Rib identifies a logical component instance by a **instance name**. A bare **`instance()`** in source gets a **fresh unique name** each time, so two bindings like `let x = instance(); let y = instance();` refer to **two independent** guest instances (separate state). To share state, use the same string for **`instance("name")`** on each binding that should alias the same guest.

**Names from `let` carry across lines** — If you type `let x = …`, later lines can use **`x`** again, the same way variables work in any REPL. That is ordinary **`let`** behaviour, not a separate concept: the session keeps the values you already defined so you do not repeat large literals or constructor calls on every line.
**Embedder responsibility** — Your `ComponentFunctionInvoke` (or equivalent) implementation must treat that worker name as the key: **one** Wasm `Instance` (or your runtime’s equivalent) per distinct name for the lifetime of the REPL session, unless you deliberately reload. Anonymous `instance()` calls must each map to a **unique** instance. This is how Wasmtime and other hosts preserve the semantics above.

**Names from `let` carry across lines** — If you type `let x = …`, later lines can use **`x`** again, the same way variables work in any REPL. That is ordinary **`let`** behaviour: the session keeps the values you already defined so you do not repeat large literals or constructor calls on every line.

**Static checking** — `rib-lang` type-checks input against the component metadata registered for the session. Many errors surface as **Rib diagnostics** prior to any Wasm export call, reducing noisy trap-driven failures during exploration.

Expand All @@ -42,7 +44,7 @@ Example: Once the component is loaded, the following will result in `3`, if the
After load, component authors and runtime integrators routinely need to:

- Exercise exports with **realistic** `record`, `variant`, `list`, and `result` values without a new Rust `main` per experiment.
- Retain **one instance** while calling constructors, methods, or other stateful exports in sequence.
- Retain **one guest instance per worker name** while calling constructors, methods, or other stateful exports in sequence (several anonymous `instance()` calls ⇒ several independent instances).
- Reuse **[Wasm Wave](https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-wave)**-compatible value text where possible instead of bespoke string formats.

`rib-repl` packages those requirements behind **`RibRepl::bootstrap`** / **`RibReplConfig`**: link the crate, implement **`ComponentFunctionInvoke`**, supply dependency loading, and obtain a `rustyline`-driven loop on top of the same **`rib`** pipeline tests may invoke directly.
Expand All @@ -60,7 +62,7 @@ After load, component authors and runtime integrators routinely need to:
- Syntax-highlighted input (`rustyline` and crate-local `RibEdit`).
- Tab completion for exports, variants, enums, and related symbols; **call completion** can insert **Wave-formatted argument lists** generated from each parameter’s **`WitType`** (see `value_generator.rs` and `rib_edit.rs`).
- Static typing of Rib source against the session’s component view.
- Stateful sessions: one guest instance for the session. Example: `let x = instance(); let a = x.increment_and_get(); let b = x.increment_and_get(); a + b` will result in `3` if the component exports a stateful `increment_and_get` method.
- Stateful exports on **one** binding: e.g. `let x = instance(); let a = x.increment-and-get(); let b = x.increment-and-get(); a + b` yields `3` if the component is stateful—because **`x`** is a single worker. A **second** `let y = instance();` is a **different** worker (fresh state); use `instance("same-name")` when you need two Rib variables to share one guest.
- Narrow embedding surface with **`rib`** and common error types **re-exported** from `rib-repl` so many projects only add one dependency.

---
Expand Down
12 changes: 4 additions & 8 deletions rib-repl/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,11 @@ pub fn get_identifiers(inferred_expr: &InferredExpr) -> Vec<VariableId> {
let mut identifiers = Vec::new();

visit_post_order_rev_mut(&mut expr, &mut |expr| match expr {
Expr::Let { variable_id, .. } => {
if !identifiers.contains(variable_id) {
identifiers.push(variable_id.clone());
}
Expr::Let { variable_id, .. } if !identifiers.contains(variable_id) => {
identifiers.push(variable_id.clone());
}
Expr::Identifier { variable_id, .. } => {
if !identifiers.contains(variable_id) {
identifiers.push(variable_id.clone());
}
Expr::Identifier { variable_id, .. } if !identifiers.contains(variable_id) => {
identifiers.push(variable_id.clone());
}
_ => {}
});
Expand Down
35 changes: 30 additions & 5 deletions rib-repl/src/invoke.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
use crate::repl_state::ReplState;
use crate::rib_val::RibVal;
use async_trait::async_trait;
use rib::wit_type::WitType;
use rib::ValueAndType;
use rib::{
ComponentDependencyKey, EvaluatedFnArgs, EvaluatedFqFn, EvaluatedWorkerName, InstructionId,
RibComponentFunctionInvoke, RibFunctionInvokeResult,
};
use std::convert::TryFrom;
use std::sync::Arc;
use uuid::Uuid;

fn io_other_box(err: impl std::fmt::Display) -> Box<dyn std::error::Error + Send + Sync> {
Box::new(std::io::Error::other(err.to_string()))
}

#[async_trait]
pub trait ComponentFunctionInvoke {
async fn invoke(
Expand All @@ -17,9 +23,9 @@ pub trait ComponentFunctionInvoke {
component_name: &str,
worker_name: &str,
function_name: &str,
args: Vec<ValueAndType>,
args: Vec<RibVal>,
return_type: Option<WitType>,
) -> anyhow::Result<Option<ValueAndType>>;
) -> anyhow::Result<Option<RibVal>>;
}

// Note: Currently, the Rib interpreter supports only one component, so the
Expand Down Expand Up @@ -62,6 +68,12 @@ impl RibComponentFunctionInvoke for ReplRibFunctionInvoke {
match self.get_cached_result(instruction_id) {
Some(result) => Ok(result),
None => {
let return_ty = return_type.clone();
let mut args_rt = Vec::with_capacity(args.0.len());
for a in &args.0 {
args_rt.push(RibVal::try_from(a).map_err(|e| io_other_box(&e))?);
}

let rib_invocation_result = self
.repl_state
.worker_function_invoke()
Expand All @@ -70,17 +82,30 @@ impl RibComponentFunctionInvoke for ReplRibFunctionInvoke {
&component_dependency.component_name,
&worker_name.0,
&function_name.0,
args.0,
args_rt,
return_type,
)
.await;

match rib_invocation_result {
Ok(result) => {
let mapped: Option<ValueAndType> = match (result, return_ty) {
(None, _) => None,
(Some(rv), Some(ty)) => Some(
rv.try_to_value_and_type(&ty)
.map_err(|e| io_other_box(&e))?,
),
(Some(_), None) => {
return Err(io_other_box(
"host returned a value but call has no return type",
));
}
};

self.repl_state
.update_cache(instruction_id.clone(), result.clone());
.update_cache(instruction_id.clone(), mapped.clone());

Ok(result)
Ok(mapped)
}
Err(err) => Err(err.into()),
}
Expand Down
11 changes: 10 additions & 1 deletion rib-repl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@
pub use anyhow;
pub use uuid;

/// Core Rib compiler and value types; pulled in transitively so embedders only add `rib-repl`.
/// Core Rib compiler crate (`rib-lang`); pulled in transitively so embedders only add `rib-repl`.
pub use rib;

/// [`rib::wit_type`] at the crate root so embedders avoid a direct `rib` / `rib-lang` dependency path.
pub use rib::wit_type;

pub use rib::{
ComponentDependency, ComponentDependencyKey, ParsedFunctionName, ParsedFunctionSite,
};

pub use command::*;
pub use dependency_manager::*;
pub use invoke::*;
Expand All @@ -17,6 +24,7 @@ pub use repl_printer::*;
pub use rib_context::*;
pub use rib_execution_error::*;
pub use rib_repl::*;
pub use rib_val::{tuple_element_type, RibVal};

mod command;
mod compiler;
Expand All @@ -32,6 +40,7 @@ mod rib_context;
mod rib_edit;
mod rib_execution_error;
mod rib_repl;
mod rib_val;
mod value_generator;

#[cfg(test)]
Expand Down
Loading
Loading