diff --git a/README.md b/README.md index 8e61ed1..693d87e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index ca5724f..0000000 --- a/docs/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Documentation - -| Guide | Description | -|-------|-------------| -| [Rib language guide](https://golemcloud.github.io/rib/guide.html) | Syntax, patterns, and REPL-oriented usage; links to a highlighted [Example WIT](https://golemcloud.github.io/rib/example-wit.html) page in the same book. | -| [`example.wit`](example.wit) | World `guide-demo`: `inventory` (records, enums, …) and `shopping` (`resource cart`); source in-repo, [readable in the book](https://golemcloud.github.io/rib/example-wit.html) with highlighting. | - -Formal grammar: [rib-lang/README.md](../rib-lang/README.md). diff --git a/docs/language-guide.md b/docs/language-guide.md index 19e2bed..c7c1345 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -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) @@ -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. + + +### 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 diff --git a/rib-lang/src/profile.rs b/rib-lang/src/profile.rs index 79048d8..41e982e 100644 --- a/rib-lang/src/profile.rs +++ b/rib-lang/src/profile.rs @@ -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; @@ -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()); diff --git a/rib-lang/src/type_inference/global_variable_type_binding.rs b/rib-lang/src/type_inference/global_variable_type_binding.rs index 94ef234..836ffc5 100644 --- a/rib-lang/src/type_inference/global_variable_type_binding.rs +++ b/rib-lang/src/type_inference/global_variable_type_binding.rs @@ -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, diff --git a/rib-lang/src/type_inference/identify_instance_creation.rs b/rib-lang/src/type_inference/identify_instance_creation.rs index e9ad8e1..cd36c03 100644 --- a/rib-lang/src/type_inference/identify_instance_creation.rs +++ b/rib-lang/src/type_inference/identify_instance_creation.rs @@ -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()); - } } _ => {} } diff --git a/rib-repl/README.md b/rib-repl/README.md index 8e0fd9a..e77af99 100644 --- a/rib-repl/README.md +++ b/rib-repl/README.md @@ -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. @@ -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. @@ -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. --- diff --git a/rib-repl/src/compiler.rs b/rib-repl/src/compiler.rs index 501b0ee..120ddeb 100644 --- a/rib-repl/src/compiler.rs +++ b/rib-repl/src/compiler.rs @@ -114,15 +114,11 @@ pub fn get_identifiers(inferred_expr: &InferredExpr) -> Vec { 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()); } _ => {} }); diff --git a/rib-repl/src/invoke.rs b/rib-repl/src/invoke.rs index 6ad1183..eb22a10 100644 --- a/rib-repl/src/invoke.rs +++ b/rib-repl/src/invoke.rs @@ -1,4 +1,5 @@ use crate::repl_state::ReplState; +use crate::rib_val::RibVal; use async_trait::async_trait; use rib::wit_type::WitType; use rib::ValueAndType; @@ -6,9 +7,14 @@ 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 { + Box::new(std::io::Error::other(err.to_string())) +} + #[async_trait] pub trait ComponentFunctionInvoke { async fn invoke( @@ -17,9 +23,9 @@ pub trait ComponentFunctionInvoke { component_name: &str, worker_name: &str, function_name: &str, - args: Vec, + args: Vec, return_type: Option, - ) -> anyhow::Result>; + ) -> anyhow::Result>; } // Note: Currently, the Rib interpreter supports only one component, so the @@ -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() @@ -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 = 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()), } diff --git a/rib-repl/src/lib.rs b/rib-repl/src/lib.rs index 0866355..e27ab91 100644 --- a/rib-repl/src/lib.rs +++ b/rib-repl/src/lib.rs @@ -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::*; @@ -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; @@ -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)] diff --git a/rib-repl/src/rib_val.rs b/rib-repl/src/rib_val.rs new file mode 100644 index 0000000..0eff817 --- /dev/null +++ b/rib-repl/src/rib_val.rs @@ -0,0 +1,369 @@ +use std::convert::TryFrom; + +use anyhow::{anyhow, bail, Context, Result}; +use rib::wit_type::{TypeEnum, WitType}; +use rib::{Value, ValueAndType}; + +/// A **portable, component-model-shaped** value for host calls and external integrations. +/// +/// # Role +/// +/// `RibVal` exists so **embedders** (Wasmtime CLI, custom hosts, tests) and **REPL plumbing** +/// can pass arguments and results across a small, stable API. Its variants mirror the usual +/// WebAssembly **component model** runtime shape (the same broad case structure as embedders like +/// Wasmtime use for component `Val`), so mapping to or from that host representation is mostly +/// structural recursion—not a parallel type system. +/// +/// # Versus [`Value`] / [`ValueAndType`] +/// +/// Inside Rib, execution uses [`Value`] together with [`WitType`] as [`ValueAndType`]. That pair +/// is the full interpreter representation. `RibVal` is **not** a replacement for that; it is the +/// **narrow hand-off** when leaving the interpreter to call into a host implementation of an +/// import. Use [`TryFrom`] from `&`[`ValueAndType`] and [`RibVal::try_to_value_and_type`] at those +/// edges when you still need full Rib values; use `RibVal` alone when the other side only speaks in +/// component-shaped values. +/// +/// # Conversions +/// +/// - Interpreter → `RibVal`: implement [`TryFrom`] for `&`[`ValueAndType`] (use [`RibVal::try_from`]). +/// - `RibVal` → interpreter: [`RibVal::try_to_value_and_type`] (needs the result [`WitType`]; we +/// cannot offer `TryInto` here without a second type parameter, and we cannot +/// implement foreign `TryFrom` targets for orphan-rule reasons). +#[derive(Debug, Clone, PartialEq)] +pub enum RibVal { + Bool(bool), + S8(i8), + U8(u8), + S16(i16), + U16(u16), + S32(i32), + U32(u32), + S64(i64), + U64(u64), + Float32(f32), + Float64(f64), + Char(char), + String(String), + List(Vec), + Record(Vec<(String, RibVal)>), + Tuple(Vec), + Variant(String, Option>), + Enum(String), + Option(Option>), + Result(Result>, Option>>), + Flags(Vec), + /// Resource handle (URI + id); embedders map to/from component resources. + Handle { + uri: String, + resource_id: u64, + }, +} + +impl TryFrom<&ValueAndType> for RibVal { + type Error = anyhow::Error; + + /// Converts a Rib [`ValueAndType`] into a [`RibVal`] (e.g. before [`crate::ComponentFunctionInvoke::invoke`]). + fn try_from(v: &ValueAndType) -> Result { + try_value_to_rib_val(&v.value, &v.typ) + } +} + +fn try_value_to_rib_val(value: &Value, ty: &WitType) -> Result { + use RibVal as R; + use WitType as WT; + + Ok(match (ty, value) { + (WT::Bool(_), Value::Bool(b)) => R::Bool(*b), + (WT::S8(_), Value::S8(x)) => R::S8(*x), + (WT::U8(_), Value::U8(x)) => R::U8(*x), + (WT::S16(_), Value::S16(x)) => R::S16(*x), + (WT::U16(_), Value::U16(x)) => R::U16(*x), + (WT::S32(_), Value::S32(x)) => R::S32(*x), + (WT::U32(_), Value::U32(x)) => R::U32(*x), + (WT::S64(_), Value::S64(x)) => R::S64(*x), + (WT::U64(_), Value::U64(x)) => R::U64(*x), + (WT::F32(_), Value::F32(x)) => R::Float32(*x), + (WT::F64(_), Value::F64(x)) => R::Float64(*x), + (WT::Chr(_), Value::Char(c)) => R::Char(*c), + (WT::Str(_), Value::String(s)) => R::String(s.clone()), + (WT::List(l), Value::List(items)) => { + let inner = &*l.inner; + R::List( + items + .iter() + .map(|item| try_value_to_rib_val(item, inner).with_context(|| "list element")) + .collect::>()?, + ) + } + (WT::Record(rec), Value::Record(vals)) => { + if rec.fields.len() != vals.len() { + bail!("record field count mismatch"); + } + let pairs: Vec<(String, RibVal)> = rec + .fields + .iter() + .zip(vals.iter()) + .map(|(f, v)| { + Ok(( + f.name.clone(), + try_value_to_rib_val(v, &f.typ).with_context(|| f.name.clone())?, + )) + }) + .collect::>()?; + R::Record(pairs) + } + (WT::Tuple(tt), Value::Tuple(items)) => { + if tt.items.len() != items.len() { + bail!("tuple arity mismatch"); + } + R::Tuple( + tt.items + .iter() + .zip(items.iter()) + .enumerate() + .map(|(i, (t, v))| { + try_value_to_rib_val(v, t).with_context(|| format!("tuple field {i}")) + }) + .collect::>()?, + ) + } + ( + WT::Variant(var_ty), + Value::Variant { + case_idx, + case_value, + }, + ) => { + let case = var_ty + .cases + .get(*case_idx as usize) + .ok_or_else(|| anyhow!("invalid variant case index"))?; + let name = case.name.clone(); + let payload = match (&case.typ, case_value) { + (None, None) => None, + (Some(inner), Some(b)) => Some(Box::new(try_value_to_rib_val(b, inner)?)), + _ => bail!("variant payload mismatch"), + }; + R::Variant(name, payload) + } + (WT::Enum(TypeEnum { cases, .. }), Value::Enum(idx)) => { + let s = cases + .get(*idx as usize) + .cloned() + .ok_or_else(|| anyhow!("invalid enum discriminant"))?; + R::Enum(s) + } + (WT::Flags(ft), Value::Flags(bits)) => { + let names: Vec = ft + .names + .iter() + .enumerate() + .filter(|&(i, _n)| bits.get(i).copied().unwrap_or(false)) + .map(|(_i, n)| n.clone()) + .collect(); + R::Flags(names) + } + (WT::Option(ot), Value::Option(inner)) => { + let mapped = match inner { + None => None, + Some(b) => Some(Box::new(try_value_to_rib_val(b, &ot.inner)?)), + }; + R::Option(mapped) + } + (WT::Result(rt), Value::Result(inner)) => { + let mapped = match inner { + Ok(v) => Ok(match v { + None => None, + Some(b) => Some(Box::new(try_value_to_rib_val( + b, + rt.ok + .as_deref() + .ok_or_else(|| anyhow!("result ok type missing"))?, + )?)), + }), + Err(v) => Err(match v { + None => None, + Some(b) => Some(Box::new(try_value_to_rib_val( + b, + rt.err + .as_deref() + .ok_or_else(|| anyhow!("result err type missing"))?, + )?)), + }), + }; + R::Result(mapped) + } + (WT::Handle(_), Value::Handle { uri, resource_id }) => R::Handle { + uri: uri.clone(), + resource_id: *resource_id, + }, + _ => bail!( + "cannot convert Rib value to RibVal for type {:?}: {:?}", + ty, + value + ), + }) +} + +impl RibVal { + /// Converts this value back into Rib’s [`ValueAndType`], using the call’s result [`WitType`] + /// from the signature (e.g. after a host returns a [`RibVal`]). + pub fn try_to_value_and_type(&self, ty: &WitType) -> Result { + rib_val_to_value_and_type(self, ty) + } +} + +fn rib_val_to_value_and_type(rv: &RibVal, ty: &WitType) -> Result { + use RibVal as R; + use WitType as WT; + + let value = match (ty, rv) { + (WT::Bool(_), R::Bool(b)) => Value::Bool(*b), + (WT::S8(_), R::S8(x)) => Value::S8(*x), + (WT::U8(_), R::U8(x)) => Value::U8(*x), + (WT::S16(_), R::S16(x)) => Value::S16(*x), + (WT::U16(_), R::U16(x)) => Value::U16(*x), + (WT::S32(_), R::S32(x)) => Value::S32(*x), + (WT::U32(_), R::U32(x)) => Value::U32(*x), + (WT::S64(_), R::S64(x)) => Value::S64(*x), + (WT::U64(_), R::U64(x)) => Value::U64(*x), + (WT::F32(_), R::Float32(x)) => Value::F32(*x), + (WT::F64(_), R::Float64(x)) => Value::F64(*x), + (WT::Chr(_), R::Char(c)) => Value::Char(*c), + (WT::Str(_), R::String(s)) => Value::String(s.clone()), + (WT::List(l), R::List(items)) => { + let inner = items + .iter() + .map(|x| { + rib_val_to_value_and_type(x, &l.inner) + .map(|v| v.value) + .with_context(|| "list element") + }) + .collect::>()?; + Value::List(inner) + } + (WT::Record(rec_ty), R::Record(pairs)) => { + if rec_ty.fields.len() != pairs.len() { + bail!("record field count mismatch"); + } + let mut out = Vec::with_capacity(pairs.len()); + for (f, (n, rv)) in rec_ty.fields.iter().zip(pairs.iter()) { + if f.name != *n { + bail!( + "record field name mismatch: expected `{}`, got `{n}`", + f.name + ); + } + out.push(rib_val_to_value_and_type(rv, &f.typ)?.value); + } + Value::Record(out) + } + (WT::Tuple(tt), R::Tuple(items)) => { + if tt.items.len() != items.len() { + bail!("tuple arity mismatch"); + } + let inner = tt + .items + .iter() + .zip(items.iter()) + .map(|(t, rv)| Ok(rib_val_to_value_and_type(rv, t)?.value)) + .collect::>()?; + Value::Tuple(inner) + } + (WT::Variant(vt), R::Variant(name, payload)) => { + let (idx, case_ty) = vt + .cases + .iter() + .enumerate() + .find(|(_, c)| c.name == *name) + .map(|(i, c)| (i as u32, &c.typ)) + .ok_or_else(|| anyhow!("unknown variant case `{name}`"))?; + let case_value = match (case_ty, payload) { + (None, None) => None, + (Some(inner), Some(p)) => { + Some(Box::new(rib_val_to_value_and_type(p, inner)?.value)) + } + _ => bail!("variant payload mismatch"), + }; + Value::Variant { + case_idx: idx, + case_value, + } + } + (WT::Enum(et), R::Enum(name)) => { + let idx = et + .cases + .iter() + .position(|c| c == name) + .ok_or_else(|| anyhow!("unknown enum case `{name}`"))? as u32; + Value::Enum(idx) + } + (WT::Option(ot), R::Option(inner)) => { + let v = match inner { + None => None, + Some(b) => Some(Box::new(rib_val_to_value_and_type(b, &ot.inner)?.value)), + }; + Value::Option(v) + } + (WT::Result(rt), R::Result(inner)) => { + let v = match inner { + Ok(x) => Ok(match x { + None => None, + Some(b) => Some(Box::new( + rib_val_to_value_and_type( + b, + rt.ok.as_deref().ok_or_else(|| anyhow!("ok type"))?, + )? + .value, + )), + }), + Err(x) => Err(match x { + None => None, + Some(b) => Some(Box::new( + rib_val_to_value_and_type( + b, + rt.err.as_deref().ok_or_else(|| anyhow!("err type"))?, + )? + .value, + )), + }), + }; + Value::Result(v) + } + (WT::Flags(ft), R::Flags(names)) => { + let mut bits = vec![false; ft.names.len()]; + for n in names { + let i = ft + .names + .iter() + .position(|x| x == n) + .ok_or_else(|| anyhow!("unknown flag `{n}`"))?; + bits[i] = true; + } + Value::Flags(bits) + } + (WT::Handle(_), R::Handle { uri, resource_id }) => Value::Handle { + uri: uri.clone(), + resource_id: *resource_id, + }, + _ => bail!( + "cannot convert RibVal to Rib value for type {:?}: {:?}", + ty, + rv + ), + }; + Ok(ValueAndType::new(value, ty.clone())) +} + +/// Returns the `i`th element [`WitType`] when the return type is a WIT tuple with multiple +/// results (helpers for splitting multi-return handling). +pub fn tuple_element_type(tuple_ty: &WitType, i: usize) -> Result { + match tuple_ty { + WitType::Tuple(t) => t + .items + .get(i) + .cloned() + .ok_or_else(|| anyhow!("tuple arity mismatch")), + _ => bail!("expected tuple return type for multi-value return"), + } +} diff --git a/rib-repl/tests/rib_val_roundtrip.rs b/rib-repl/tests/rib_val_roundtrip.rs new file mode 100644 index 0000000..c954dfe --- /dev/null +++ b/rib-repl/tests/rib_val_roundtrip.rs @@ -0,0 +1,46 @@ +use rib::wit_type::builders as wt; +use rib::wit_type::{NameTypePair, TypeRecord, WitType}; +use rib::{Value, ValueAndType}; +use std::convert::TryFrom; + +use rib_repl::RibVal; + +#[test] +fn roundtrip_u32() { + let v = ValueAndType::new(Value::U32(42), wt::u32()); + let r = RibVal::try_from(&v).unwrap(); + assert_eq!(format!("{r:?}"), "U32(42)"); + let back = r.try_to_value_and_type(&wt::u32()).unwrap(); + assert_eq!(back, v); +} + +#[test] +fn roundtrip_record() { + let typ = WitType::Record(TypeRecord { + name: None, + owner: None, + fields: vec![ + NameTypePair { + name: "a".into(), + typ: wt::u32(), + }, + NameTypePair { + name: "b".into(), + typ: wt::str(), + }, + ], + }); + let v = ValueAndType::new( + Value::Record(vec![Value::U32(1), Value::String("x".into())]), + typ.clone(), + ); + let r = RibVal::try_from(&v).unwrap(); + let RibVal::Record(p) = &r else { + panic!("expected record"); + }; + assert_eq!(p.len(), 2); + assert_eq!(p[0].0, "a"); + assert_eq!(p[1].0, "b"); + let back = r.try_to_value_and_type(&typ).unwrap(); + assert_eq!(back, v); +}