diff --git a/docs/language-guide.md b/docs/language-guide.md index c7c1345..ae1fea0 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -64,7 +64,7 @@ We document a lot so nothing feels hidden—but Rib is not heavyweight. For REPL --- -Companion WIT — [`example.wit`](example.wit) exports `inventory` (records, enums, plain funcs) and `shopping` (a `cart` resource) from world `guide-demo`. Start with [§1](#1-instance-and-calling-exports); [§0](#0-example-wit-examplewit) spells out the WIT if you want it on the page, and [§9 *`match`*](#9-match-and-patterns) onward when you need more than export calls. +Companion WIT — [`example.wit`](example.wit) exports `inventory` (records, enums, plain funcs) and `shopping` (a `cart` resource) from world `guide-demo`. Start with [§1](#1-instance-and-calling-exports); [§0](#0-example-wit-examplewit) spells out the WIT if you want it on the page, and [§9 *`match`*](#9-match-and-patterns) onward when you need more than export calls. [§16](#16-common-compilation-errors-examples) lists typical **compile-time** mistakes (arity, types, `match` coverage) with small broken snippets. --- @@ -86,6 +86,7 @@ Companion WIT — [`example.wit`](example.wit) exports `inventory` (records, enu 13. [`option` and `result`](#13-option-and-result) 14. [String interpolation](#14-string-interpolation) 15. [Invoking resource methods](#15-invoking-resource-methods) +16. [Common compilation errors (examples)](#16-common-compilation-errors-examples) --- @@ -425,6 +426,8 @@ match home { } ``` + + ### 9.8 Compile-time errors (`match`, enums, calls) Rib reports many mistakes **while compiling** Rib source (REPL line or script)—**before** your embedder invokes Wasm. A few common cases: @@ -435,6 +438,8 @@ Rib reports many mistakes **while compiling** Rib source (REPL line or script) **Calls** — Arguments are checked against the **`func`** signature. Example: **`validate-qty`** expects **`u32`**; passing a **string** or the wrong **record** shape to **`length`** is rejected **at compile time**, not as a failed Wasm call later. +Short **copy-paste examples** of invalid programs (and what goes wrong) are in [§16](#16-common-compilation-errors-examples). + --- ## 10. List comprehensions (`for` … `yield`) @@ -537,6 +542,56 @@ shopping-cart.line-count() --- +## 16. Common compilation errors (examples) + +Assume [`example.wit`](example.wit) is loaded and **`let my-instance = instance();`** already exists. The snippets below are **invalid** Rib: the compiler rejects them **before** any Wasm runs. Exact wording of diagnostics can change between versions; the point is **why** each program fails. + +### Wrong number of parameters + +From WIT, **`validate-qty`** takes **one** `u32`, and **`length`** takes **one** `point` record: + +```rust +// Invalid — missing argument +my-instance.validate-qty() + +// Invalid — too many arguments +my-instance.length({ x: 1, y: 2 }, { x: 0, y: 0 }) +``` + +Rib reports an **arity** / argument-count mismatch against the export’s signature. + +### Type mismatch + +**`validate-qty`** expects a **`u32`**, not a string; **`length`** expects a **`point`**, not a bare number: + +```rust +// Invalid — string where u32 is required +my-instance.validate-qty("100") + +// Invalid — s32 where a point record is required +my-instance.length(42) +``` + +The compiler compares your expression types to the WIT parameter types and stops with a **type** error. + +### Non-exhaustive pattern match (`match`) + +`variant payment-info` has **three** cases (`card`, `wallet`, `failed`). This `match` is **incomplete**—there is no arm for **`failed`**, and no **`_`** fallback: + +```rust +// Invalid — not all variant cases covered (`p` has type `payment-info`) +match p { + card(_) => "c", + wallet => "w" +} +``` + +Either add **`failed(_) => …`** or a catch-all **`_ => …`**. The same idea applies to **`enum order-stage`**: every **`draft` / `placed` / `shipped`** arm must be present, or you use **`_`**. + +Enum and variant **exhaustiveness** is checked at compile time (see also [§9.8](#sec-9-8)). + +--- + ## Quick reference card | Topic | Syntax / reminder | @@ -547,7 +602,7 @@ shopping-cart.line-count() | Inference | fixed-point over WIT; REPL tab completion (§5) | | If | `if c then a else b` | | Match | `match e { pat => x, _ => y }` | -| Compile-time | Exhaustive `match`, enum case names, call arity/types vs WIT (§9.8) | +| Compile-time | Exhaustive `match`, enum case names, call arity/types vs WIT (§9.8, §16) | | Call | `f(a, b)` or `recv.method(a)` | | Instance | `let my-instance = instance();` then `my-instance.lookup-sku(7)` (name is yours) | | For | `for x in xs { yield y; }` | diff --git a/rib-lang/src/compiler/mod.rs b/rib-lang/src/compiler/mod.rs index 9c9ca31..0127cef 100644 --- a/rib-lang/src/compiler/mod.rs +++ b/rib-lang/src/compiler/mod.rs @@ -173,14 +173,14 @@ impl RibCompilerConfig { } } -pub trait GenerateWorkerName { - fn generate_worker_name(&self) -> String; +pub trait GenerateInstanceName { + fn generate_instance_name(&self) -> String; } pub struct DefaultWorkerNameGenerator; -impl GenerateWorkerName for DefaultWorkerNameGenerator { - fn generate_worker_name(&self) -> String { +impl GenerateInstanceName for DefaultWorkerNameGenerator { + fn generate_instance_name(&self) -> String { let uuid = uuid::Uuid::new_v4(); format!("instance-{uuid}") } diff --git a/rib-lang/src/function_name.rs b/rib-lang/src/function_name.rs index 9dd7f13..ba9e600 100644 --- a/rib-lang/src/function_name.rs +++ b/rib-lang/src/function_name.rs @@ -384,6 +384,55 @@ impl ParsedFunctionName { function: self.function.clone(), } } + + /// Segments for resolving a WebAssembly component export via nested instance names, then the + /// function export name. + /// + /// Packaged WIT paths like `component:pkg/iface.{fn}` identify the interface in metadata, but + /// component worlds usually export the interface **instance at the root** as `iface`, not as a + /// nested export named `component:pkg`. Runtimes should walk `Component::get_export_index` with + /// these segments (for example `["inventory", "lookup-sku"]`), not a path derived from splitting + /// the full [`ParsedFunctionSite::interface_name`] string. + pub fn wasm_component_export_path(&self) -> Vec { + let mut segments: Vec = match &self.site { + ParsedFunctionSite::Global => Vec::new(), + ParsedFunctionSite::Interface { name } => name + .split('/') + .filter(|s| !s.is_empty()) + .map(str::to_string) + .collect(), + ParsedFunctionSite::PackagedInterface { interface, .. } => vec![interface.clone()], + }; + let leaf = match &self.function { + ParsedFunctionReference::Function { function } => function.clone(), + _ => self.function.function_name(), + }; + segments.push(leaf); + segments + } + + /// Like [`wasm_component_export_path`](Self::wasm_component_export_path), but includes common + /// alternate spellings for the **last** segment (WIT kebab-case vs snake_case in lowered names). + pub fn wasm_component_export_path_candidates(&self) -> Vec> { + let primary = self.wasm_component_export_path(); + let mut out: Vec> = Vec::new(); + let mut push = |p: Vec| { + if !out.iter().any(|e| e == &p) { + out.push(p); + } + }; + push(primary.clone()); + if let Some(last) = primary.last() { + let snake = last.replace('-', "_"); + if snake != *last { + let mut alt = primary.clone(); + alt.pop(); + alt.push(snake); + push(alt); + } + } + out + } } #[cfg(test)] @@ -434,6 +483,19 @@ mod function_name_tests { ); } + #[test] + fn wasm_component_export_path_packaged_matches_world_root() { + let parsed = + ParsedFunctionName::parse("component:rib-smoke/inventory.{lookup-sku}").expect("parse"); + assert_eq!( + parsed.wasm_component_export_path(), + vec!["inventory", "lookup-sku"] + ); + let cands = parsed.wasm_component_export_path_candidates(); + assert!(cands.iter().any(|p| p == &vec!["inventory", "lookup-sku"])); + assert!(cands.iter().any(|p| p == &vec!["inventory", "lookup_sku"])); + } + #[test] fn parse_function_name_in_exported_interface() { let parsed = ParsedFunctionName::parse("ns:name/interface.{fn1}").expect("Parsing failed"); diff --git a/rib-lang/src/interpreter/eval.rs b/rib-lang/src/interpreter/eval.rs index 67665c7..edc690e 100644 --- a/rib-lang/src/interpreter/eval.rs +++ b/rib-lang/src/interpreter/eval.rs @@ -1,5 +1,5 @@ use crate::{ - DefaultWorkerNameGenerator, Expr, GenerateWorkerName, RibCompilationError, RibCompiler, + DefaultWorkerNameGenerator, Expr, GenerateInstanceName, RibCompilationError, RibCompiler, RibCompilerConfig, RibComponentFunctionInvoke, RibInput, RibResult, RibRuntimeError, }; use std::sync::Arc; @@ -8,7 +8,7 @@ pub struct RibEvalConfig { compiler_config: RibCompilerConfig, rib_input: RibInput, function_invoke: Arc, - generate_worker_name: Arc, + generate_instance_name: Arc, } impl RibEvalConfig { @@ -16,13 +16,13 @@ impl RibEvalConfig { compiler_config: RibCompilerConfig, rib_input: RibInput, function_invoke: Arc, - generate_worker_name: Option>, + generate_worker_name: Option>, ) -> Self { RibEvalConfig { compiler_config, rib_input, function_invoke, - generate_worker_name: generate_worker_name + generate_instance_name: generate_worker_name .unwrap_or_else(|| Arc::new(DefaultWorkerNameGenerator)), } } @@ -47,7 +47,7 @@ impl RibEvaluator { compiled.byte_code, self.config.rib_input, self.config.function_invoke, - Some(self.config.generate_worker_name.clone()), + Some(self.config.generate_instance_name.clone()), ) .await?; diff --git a/rib-lang/src/interpreter/interpreter_stack_value.rs b/rib-lang/src/interpreter/interpreter_stack_value.rs index 8326f1e..bc9ee1d 100644 --- a/rib-lang/src/interpreter/interpreter_stack_value.rs +++ b/rib-lang/src/interpreter/interpreter_stack_value.rs @@ -210,9 +210,15 @@ impl fmt::Debug for RibInterpreterStackValue { RibInterpreterStackValue::Unit => "unit".to_string(), RibInterpreterStackValue::Val(value) => { match &value.value { - Value::Handle { uri, resource_id } => { + Value::Handle { + uri, + resource_id, + instance_name, + } => { // wasm-wave don't support resource handles yet - format!("handle:{{uri:{uri}, resource-id:{resource_id}}}") + format!( + "handle:{{uri:{uri}, resource-id:{resource_id}, instance:{instance_name}}}" + ) } _ => value.to_string(), diff --git a/rib-lang/src/interpreter/mod.rs b/rib-lang/src/interpreter/mod.rs index 1988cc0..acb08c3 100644 --- a/rib-lang/src/interpreter/mod.rs +++ b/rib-lang/src/interpreter/mod.rs @@ -20,14 +20,14 @@ mod rib_interpreter; mod rib_runtime_error; mod stack; -use crate::{DefaultWorkerNameGenerator, GenerateWorkerName, RibByteCode}; +use crate::{DefaultWorkerNameGenerator, GenerateInstanceName, RibByteCode}; use std::sync::Arc; pub async fn interpret( rib: RibByteCode, rib_input: RibInput, function_invoke: Arc, - generate_worker_name: Option>, + generate_worker_name: Option>, ) -> Result { let mut interpreter = Interpreter::new( rib_input, @@ -43,7 +43,7 @@ pub async fn interpret( pub async fn interpret_pure( rib: RibByteCode, rib_input: RibInput, - generate_worker_name: Option>, + generate_worker_name: Option>, ) -> Result { let mut interpreter = Interpreter::pure( rib_input, diff --git a/rib-lang/src/interpreter/rib_interpreter.rs b/rib-lang/src/interpreter/rib_interpreter.rs index 6b16ba7..fe63e14 100644 --- a/rib-lang/src/interpreter/rib_interpreter.rs +++ b/rib-lang/src/interpreter/rib_interpreter.rs @@ -6,7 +6,7 @@ use crate::interpreter::rib_runtime_error::{ }; use crate::interpreter::stack::InterpreterStack; use crate::{ - internal_corrupted_state, DefaultWorkerNameGenerator, GenerateWorkerName, RibByteCode, + internal_corrupted_state, DefaultWorkerNameGenerator, GenerateInstanceName, RibByteCode, RibComponentFunctionInvoke, RibIR, RibInput, RibResult, }; use std::sync::Arc; @@ -14,7 +14,7 @@ use std::sync::Arc; pub struct Interpreter { pub input: RibInput, pub invoke: Arc, - pub generate_worker_name: Arc, + pub generate_worker_name: Arc, } impl Default for Interpreter { @@ -33,7 +33,7 @@ impl Interpreter { pub fn new( input: RibInput, invoke: Arc, - generate_worker_name: Arc, + generate_worker_name: Arc, ) -> Self { Interpreter { input: input.clone(), @@ -46,7 +46,7 @@ impl Interpreter { // All it needs is environment with the required variables to evaluate the Rib script pub fn pure( input: RibInput, - generate_worker_name: Arc, + generate_worker_name: Arc, ) -> Self { Interpreter { input, @@ -716,7 +716,7 @@ mod internal { ) -> RibInterpreterResult<()> { match variable_id { None => { - let worker_name = interpreter.generate_worker_name.generate_worker_name(); + let worker_name = interpreter.generate_worker_name.generate_instance_name(); interpreter_stack .push_val(ValueAndType::new(Value::String(worker_name.clone()), str())); @@ -741,7 +741,7 @@ mod internal { } None => { - let worker_name = interpreter.generate_worker_name.generate_worker_name(); + let worker_name = interpreter.generate_worker_name.generate_instance_name(); interpreter_stack .push_val(ValueAndType::new(Value::String(worker_name.clone()), str())); @@ -1302,8 +1302,10 @@ mod internal { })?; match &handle.value { - Value::Handle { uri, .. } => { - let worker_name = uri.rsplit_once('/').map(|(_, last)| last).unwrap_or(uri); + Value::Handle { .. } => { + let instance_name = handle.value.handle_instance_name().ok_or( + internal_corrupted_state!("resource handle missing instance name"), + )?; final_args.push(handle.clone()); final_args.extend(parameter_values); @@ -1312,7 +1314,7 @@ mod internal { .invoke_worker_function_async( component_info, instruction_id, - worker_name.to_string(), + instance_name, function_name_cloned.clone(), final_args, expected_result_type.clone(), @@ -4257,7 +4259,7 @@ mod tests { use crate::{print_value_and_type, IntoValueAndType, Value, ValueAndType}; use crate::{ ComponentDependency, ComponentDependencyKey, DefaultWorkerNameGenerator, - EvaluatedFnArgs, EvaluatedFqFn, EvaluatedWorkerName, GenerateWorkerName, + EvaluatedFnArgs, EvaluatedFqFn, EvaluatedWorkerName, GenerateInstanceName, GetLiteralValue, InstructionId, RibComponentFunctionInvoke, RibFunctionInvokeResult, RibInput, }; @@ -4859,6 +4861,7 @@ mod tests { Value::Handle { uri, resource_id: 0, + instance_name: worker_name.clone(), }, handle(AnalysedResourceId(0), AnalysedResourceMode::Owned), ) @@ -4908,14 +4911,13 @@ mod tests { } "golem:it/api.{[static]cart.create}" => { - let uri = format!( - "urn:worker:99738bab-a3bf-4a12-8830-b6fd783d1ef2/{}", - worker_name.0 - ); + let wn = worker_name.0.clone(); + let uri = format!("urn:worker:99738bab-a3bf-4a12-8830-b6fd783d1ef2/{wn}"); let value = Value::Handle { uri, resource_id: 0, + instance_name: wn, }; Ok(Some(ValueAndType::new( @@ -4925,14 +4927,13 @@ mod tests { } "golem:it/api.{[static]cart.create-safe}" => { - let uri = format!( - "urn:worker:99738bab-a3bf-4a12-8830-b6fd783d1ef2/{}", - worker_name.0 - ); + let wn = worker_name.0.clone(); + let uri = format!("urn:worker:99738bab-a3bf-4a12-8830-b6fd783d1ef2/{wn}"); let resource = Value::Handle { uri, resource_id: 0, + instance_name: wn, }; let value = Value::Result(Ok(Some(Box::new(resource)))); @@ -5118,8 +5119,8 @@ mod tests { pub(crate) struct StaticWorkerNameGenerator; - impl GenerateWorkerName for StaticWorkerNameGenerator { - fn generate_worker_name(&self) -> String { + impl GenerateInstanceName for StaticWorkerNameGenerator { + fn generate_instance_name(&self) -> String { "test-worker".to_string() } } diff --git a/rib-lang/src/value.rs b/rib-lang/src/value.rs index d7c95c1..32b3483 100644 --- a/rib-lang/src/value.rs +++ b/rib-lang/src/value.rs @@ -24,12 +24,45 @@ pub enum Value { Flags(Vec), Option(Option>), Result(Result>, Option>>), + /// Guest resource handle at the interpreter boundary. + /// + /// - `resource_id` — embedder-defined id (for example a table slot in Wasmtime). + /// - `instance_name` — component **instance** key (`instance()` / `instance("x")`) used to route + /// host calls for resource methods. When empty, [`handle_instance_name`](Value::handle_instance_name) + /// falls back to the last `/` segment of `uri` for older embedders. Handle { uri: String, resource_id: u64, + instance_name: String, }, } +impl Value { + /// Instance key for routing resource method invocations to the correct host-side component + /// instance. Prefers [`Handle::instance_name`]; if empty, uses the last `/`-separated segment of + /// [`Handle::uri`]. + pub fn handle_instance_name(&self) -> Option { + match self { + Value::Handle { + uri, + instance_name, + resource_id: _, + } => { + if !instance_name.is_empty() { + Some(instance_name.clone()) + } else { + Some( + uri.rsplit_once('/') + .map(|(_, last)| last.to_string()) + .unwrap_or_else(|| uri.clone()), + ) + } + } + _ => None, + } + } +} + impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -107,7 +140,11 @@ impl std::fmt::Display for Value { Value::Result(Ok(None)) => write!(f, "ok"), Value::Result(Err(Some(v))) => write!(f, "err({v})"), Value::Result(Err(None)) => write!(f, "err"), - Value::Handle { uri, resource_id } => write!(f, "handle({uri}#{resource_id})"), + Value::Handle { + uri, + resource_id, + instance_name, + } => write!(f, "handle({uri}#{resource_id} @ {instance_name})",), } } } diff --git a/rib-repl/src/export_path.rs b/rib-repl/src/export_path.rs new file mode 100644 index 0000000..6de52a6 --- /dev/null +++ b/rib-repl/src/export_path.rs @@ -0,0 +1,273 @@ +//! Map a **call label** (what the Rib runtime passes to the host) to Wasm export path segments using +//! [`WitExport`] metadata only. +//! +//! REPL / Wasmtime flows use short names like `lookup-sku` or `inventory/lookup-sku`, not Golem-style +//! fully qualified [`ParsedFunctionName`] strings. This module does not parse that grammar. + +use rib::wit_type::WitExport; + +/// Component-type walks often include a leading `namespace:package` segment that is **not** a real +/// world export name; the world still exports interface instances at the root (`inventory`, …). +/// Embedders walk `get_export_index` with that world-local path, so we drop one leading segment when +/// it looks like a WIT package id (`:`). +fn strip_leading_packaged_namespace(mut path: Vec) -> Vec { + if path.len() >= 2 && path[0].contains(':') { + path.remove(0); + } + path +} + +fn push_normalized_path(out: &mut Vec>, path: Vec) { + let path = strip_leading_packaged_namespace(path); + if !path.is_empty() && !out.iter().any(|e| e == &path) { + out.push(path); + } +} + +/// All export paths derived from [`WitExport`] (nested instance names + function), in the same shape +/// embedders use with `get_export_index` — **after** normalizing away a leading packaged namespace +/// segment when present. +pub fn wasm_export_paths_from_wit(exports: &[WitExport]) -> Vec> { + let mut out = Vec::new(); + for e in exports { + match e { + WitExport::Function(f) => push_normalized_path(&mut out, vec![f.name.clone()]), + WitExport::Interface(iface) => { + let prefix: Vec = iface + .name + .split('/') + .filter(|s| !s.is_empty()) + .map(str::to_string) + .collect(); + for f in &iface.functions { + let mut p = prefix.clone(); + p.push(f.name.clone()); + push_normalized_path(&mut out, p); + } + } + } + } + out +} + +struct ExportQuery { + leaf: String, + /// When `None`, only the function leaf is used (must be unique across exports). + interface_prefix: Option>, +} + +/// Turn a runtime call label into a leaf name and optional interface path. +/// +/// Accepted shapes (no `ParsedFunctionName` / Golem FQN parser): +/// - `lookup-sku` — function leaf only +/// - `inventory/lookup-sku` — interface path + function (segments separated by `/`) +/// - `inventory.{lookup-sku}` — optional brace form: text before `.{` gives the interface (last path +/// segment after `/` if present, else the whole prefix) +fn parse_call_label(label: &str) -> ExportQuery { + let label = label.trim(); + + if let Some(idx) = label.rfind(".{") { + if let Some(rest) = label.get(idx + 2..) { + if let Some(end) = rest.find('}') { + let leaf = rest[..end].to_string(); + let before = label[..idx].trim_end_matches('.'); + let interface_prefix = if before.is_empty() { + None + } else if let Some((_, last)) = before.rsplit_once('/') { + Some(vec![last.to_string()]) + } else { + Some(vec![before.to_string()]) + }; + return ExportQuery { + leaf, + interface_prefix, + }; + } + } + } + + if label.contains('/') { + let parts: Vec<&str> = label.split('/').filter(|s| !s.is_empty()).collect(); + if parts.len() >= 2 { + let leaf = parts[parts.len() - 1].to_string(); + let prefix = parts[..parts.len() - 1] + .iter() + .map(|s| s.to_string()) + .collect(); + return ExportQuery { + leaf, + interface_prefix: Some(prefix), + }; + } + } + + ExportQuery { + leaf: label.to_string(), + interface_prefix: None, + } +} + +fn names_equivalent_wit_abi(a: &str, b: &str) -> bool { + a == b || a.replace('-', "_") == b.replace('-', "_") +} + +/// Rib call sites use `resource.new` inside braces; WIT export metadata uses `[constructor]resource` +/// (see [`WitFunction::is_constructor`] in `rib-lang`). Method calls use `resource.method` and +/// metadata uses `[method]resource.method` ([`WitFunction::is_method`]). +fn call_leaf_matches_wit_export_name(call_leaf: &str, export_name: &str) -> bool { + if names_equivalent_wit_abi(call_leaf, export_name) { + return true; + } + if let Some(resource) = call_leaf.strip_suffix(".new") { + if let Some(res) = export_name.strip_prefix("[constructor]") { + return names_equivalent_wit_abi(resource, res); + } + } + if let Some(rest) = export_name.strip_prefix("[method]") { + if names_equivalent_wit_abi(call_leaf, rest) { + return true; + } + } + if let Some(rest) = export_name.strip_prefix("[static]") { + if names_equivalent_wit_abi(call_leaf, rest) { + return true; + } + } + false +} + +fn path_matches_query(path: &[String], query: &ExportQuery) -> bool { + let Some(last) = path.last() else { + return false; + }; + if !call_leaf_matches_wit_export_name(&query.leaf, last) { + return false; + } + let iface = &path[..path.len().saturating_sub(1)]; + match &query.interface_prefix { + None => true, + Some(want) if want.is_empty() => true, + Some(want) => iface == want.as_slice() || iface.ends_with(want.as_slice()), + } +} + +/// Resolve a call label to the unique Wasm export path for this component's [`WitExport`] list. +pub fn resolve_wasm_export_path( + exports: &[WitExport], + function_name: &str, +) -> Result, String> { + let paths = wasm_export_paths_from_wit(exports); + let query = parse_call_label(function_name); + let matches: Vec<&Vec> = paths + .iter() + .filter(|p| path_matches_query(p, &query)) + .collect(); + match matches.len() { + 0 => { + let sample: Vec = paths + .iter() + .take(24) + .map(|p| p.join("/")) + .collect(); + Err(format!( + "no export matches `{function_name}`. Example paths: [{}]", + sample.join(", ") + )) + } + 1 => Ok(matches[0].clone()), + _ => Err(format!( + "ambiguous `{function_name}`: {} matching exports (use a path like `interface/function-name`)", + matches.len() + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_r::test; + + use rib::wit_type::{WitFunction, WitInterface}; + + fn sample_inventory_export() -> Vec { + vec![WitExport::Interface(WitInterface { + name: "inventory".to_string(), + functions: vec![WitFunction { + name: "lookup-sku".to_string(), + parameters: vec![], + result: None, + }], + })] + } + + #[test] + fn bare_function_name_when_unique() { + let exports = sample_inventory_export(); + let p = resolve_wasm_export_path(&exports, "lookup-sku").expect("resolve"); + assert_eq!(p, vec!["inventory", "lookup-sku"]); + } + + #[test] + fn interface_slash_function() { + let exports = sample_inventory_export(); + let p = resolve_wasm_export_path(&exports, "inventory/lookup-sku").expect("resolve"); + assert_eq!(p, vec!["inventory", "lookup-sku"]); + } + + #[test] + fn brace_form_resolves_interface_leaf() { + let exports = sample_inventory_export(); + let p = resolve_wasm_export_path(&exports, "inventory.{lookup-sku}").expect("resolve"); + assert_eq!(p, vec!["inventory", "lookup-sku"]); + } + + /// Mirrors Wasmtime `component_exports`: interface key is `path[..-1].join("/")`, which can start + /// with `component:pkg/...`. The real world export path must not keep the package segment. + #[test] + fn strips_leading_namespace_package_from_wit_metadata_paths() { + let exports = vec![WitExport::Interface(WitInterface { + name: "component:rib-smoke/inventory".to_string(), + functions: vec![WitFunction { + name: "lookup-sku".to_string(), + parameters: vec![], + result: None, + }], + })]; + let paths = wasm_export_paths_from_wit(&exports); + assert_eq!(paths, vec![vec!["inventory", "lookup-sku"]]); + + let p = resolve_wasm_export_path(&exports, "component:rib-smoke/inventory.{lookup-sku}") + .expect("resolve"); + assert_eq!(p, vec!["inventory", "lookup-sku"]); + } + + #[test] + fn resource_constructor_cart_new_matches_braced_call() { + let exports = vec![WitExport::Interface(WitInterface { + name: "component:rib-smoke/shopping".to_string(), + functions: vec![WitFunction { + name: "[constructor]cart".to_string(), + parameters: vec![], + result: None, + }], + })]; + let p = resolve_wasm_export_path(&exports, "component:rib-smoke/shopping.{cart.new}") + .expect("resolve"); + assert_eq!(p, vec!["shopping", "[constructor]cart"]); + } + + #[test] + fn resource_method_cart_add_line_matches_braced_call() { + let exports = vec![WitExport::Interface(WitInterface { + name: "component:rib-smoke/shopping".to_string(), + functions: vec![WitFunction { + name: "[method]cart.add-line".to_string(), + parameters: vec![], + result: None, + }], + })]; + let p = resolve_wasm_export_path(&exports, "component:rib-smoke/shopping.{cart.add-line}") + .expect("resolve"); + assert_eq!(p, vec!["shopping", "[method]cart.add-line"]); + } +} diff --git a/rib-repl/src/instance_name_gen.rs b/rib-repl/src/instance_name_gen.rs index 3412650..55b54d1 100644 --- a/rib-repl/src/instance_name_gen.rs +++ b/rib-repl/src/instance_name_gen.rs @@ -1,5 +1,5 @@ use crate::repl_state::ReplState; -use rib::GenerateWorkerName; +use rib::GenerateInstanceName; use std::collections::HashMap; use std::sync::Arc; @@ -52,8 +52,8 @@ impl DynamicWorkerGen { } } -impl GenerateWorkerName for DynamicWorkerGen { - fn generate_worker_name(&self) -> String { +impl GenerateInstanceName for DynamicWorkerGen { + fn generate_instance_name(&self) -> String { self.repl_state.generate_worker_name() } } diff --git a/rib-repl/src/invoke.rs b/rib-repl/src/invoke.rs index eb22a10..aeb8edd 100644 --- a/rib-repl/src/invoke.rs +++ b/rib-repl/src/invoke.rs @@ -21,7 +21,7 @@ pub trait ComponentFunctionInvoke { &self, component_id: Uuid, component_name: &str, - worker_name: &str, + instance_name: &str, function_name: &str, args: Vec, return_type: Option, diff --git a/rib-repl/src/lib.rs b/rib-repl/src/lib.rs index e27ab91..c4d5e1b 100644 --- a/rib-repl/src/lib.rs +++ b/rib-repl/src/lib.rs @@ -17,6 +17,7 @@ pub use rib::{ pub use command::*; pub use dependency_manager::*; +pub use export_path::{resolve_wasm_export_path, wasm_export_paths_from_wit}; pub use invoke::*; pub use raw::*; pub use repl_bootstrap_error::*; @@ -30,6 +31,7 @@ mod command; mod compiler; mod dependency_manager; mod eval; +mod export_path; mod instance_name_gen; mod invoke; mod raw; diff --git a/rib-repl/src/repl_printer.rs b/rib-repl/src/repl_printer.rs index 1a12047..dd5fb1c 100644 --- a/rib-repl/src/repl_printer.rs +++ b/rib-repl/src/repl_printer.rs @@ -710,14 +710,19 @@ fn display_for_value_and_type(value_and_type: &ValueAndType) -> String { Err(None) => "err".to_string(), } } - Value::Handle { uri, resource_id } => display_for_resource_handle(uri, resource_id), + Value::Handle { + uri, + resource_id, + instance_name, + } => display_for_resource_handle(uri, resource_id, instance_name), } } -fn display_for_resource_handle(uri: &str, resource_id: &u64) -> String { +fn display_for_resource_handle(uri: &str, resource_id: &u64, instance_name: &str) -> String { let resource = Value::Record(vec![ Value::String(uri.to_string()), Value::U64(*resource_id), + Value::String(instance_name.to_string()), ]); let analysed_type = record(vec![ @@ -729,6 +734,10 @@ fn display_for_resource_handle(uri: &str, resource_id: &u64) -> String { name: "resource-id".to_string(), typ: u64(), }, + NameTypePair { + name: "instance-name".to_string(), + typ: str(), + }, ]); let result = ValueAndType::new(resource, analysed_type); diff --git a/rib-repl/src/rib_val.rs b/rib-repl/src/rib_val.rs index 0eff817..61ab139 100644 --- a/rib-repl/src/rib_val.rs +++ b/rib-repl/src/rib_val.rs @@ -52,10 +52,12 @@ pub enum RibVal { Option(Option>), Result(Result>, Option>>), Flags(Vec), - /// Resource handle (URI + id); embedders map to/from component resources. + /// Resource handle: `resource_id` is embedder-defined; `instance_name` routes resource methods to + /// the correct component instance (see [`rib::Value::Handle`]). Handle { uri: String, resource_id: u64, + instance_name: String, }, } @@ -193,9 +195,17 @@ fn try_value_to_rib_val(value: &Value, ty: &WitType) -> Result { }; R::Result(mapped) } - (WT::Handle(_), Value::Handle { uri, resource_id }) => R::Handle { + ( + WT::Handle(_), + Value::Handle { + uri, + resource_id, + instance_name, + }, + ) => R::Handle { uri: uri.clone(), resource_id: *resource_id, + instance_name: instance_name.clone(), }, _ => bail!( "cannot convert Rib value to RibVal for type {:?}: {:?}", @@ -342,9 +352,17 @@ fn rib_val_to_value_and_type(rv: &RibVal, ty: &WitType) -> Result } Value::Flags(bits) } - (WT::Handle(_), R::Handle { uri, resource_id }) => Value::Handle { + ( + WT::Handle(_), + R::Handle { + uri, + resource_id, + instance_name, + }, + ) => Value::Handle { uri: uri.clone(), resource_id: *resource_id, + instance_name: instance_name.clone(), }, _ => bail!( "cannot convert RibVal to Rib value for type {:?}: {:?}", diff --git a/rib-repl/src/value_generator.rs b/rib-repl/src/value_generator.rs index 5fcbd25..0045dc9 100644 --- a/rib-repl/src/value_generator.rs +++ b/rib-repl/src/value_generator.rs @@ -110,6 +110,7 @@ pub fn generate_value(analysed_tpe: &WitType) -> Value { WitType::Handle(_) => Value::Handle { uri: "".to_string(), resource_id: 0, + instance_name: String::new(), }, } }