diff --git a/cmd/gravity/src/codegen/imports.rs b/cmd/gravity/src/codegen/imports.rs index f3d28b9..61fb3a7 100644 --- a/cmd/gravity/src/codegen/imports.rs +++ b/cmd/gravity/src/codegen/imports.rs @@ -21,7 +21,7 @@ use crate::{ GoIdentifier, GoResult, GoType, imports::{CONTEXT_CONTEXT, WAZERO_API_MODULE}, }, - resolve_param_type, resolve_type, resolve_wasm_type, + resolve_nested_type, resolve_param_type, resolve_type, resolve_wasm_type, }; /// Analyzer for imports - only does analysis, no code generation @@ -164,7 +164,10 @@ impl<'a> ImportAnalyzer<'a> { } fn analyze_variant_case(&self, variant_name: &str, case: &Case) -> VariantCase { - let payload = case.ty.as_ref().map(|t| resolve_type(t, self.resolve)); + let payload = case.ty.as_ref().map(|t| { + let position = format!("the payload of variant case `{}`", case.name); + resolve_nested_type(t, self.resolve, &position) + }); let dispatch = match crate::case_dispatch_kind(case, self.resolve) { crate::CaseDispatchKind::DirectRecord => CaseDispatch::DirectRecord { record_type: payload.clone().expect("DirectRecord case has a payload"), @@ -189,9 +192,10 @@ impl<'a> ImportAnalyzer<'a> { .fields .iter() .map(|field| { + let position = format!("record field `{}`", field.name); ( GoIdentifier::public(&field.name), - resolve_type(&field.ty, self.resolve), + resolve_nested_type(&field.ty, self.resolve, &position), ) }) .collect(), @@ -940,4 +944,75 @@ mod tests { other => panic!("alias analyzed as: {other:?}"), } } + + /// Analyzes a world that imports `types`, an interface declaring `decls`. + fn analyze_types(decls: &str) { + let fixture = Fixture::parse(&format!( + "package test:fixture; + interface types {{ + {decls} + }} + world test-world {{ + import types; + }}" + )); + ImportAnalyzer::new(&fixture.resolve, fixture.world()).analyze(); + } + + /// Resolves the result type of the function the world exports as `f`. + fn resolve_export_result(wit_result: &str) { + let fixture = Fixture::parse(&format!( + "package test:fixture; + world test-world {{ + export f: func() -> {wit_result}; + }}" + )); + let result = fixture.export("f").result.expect("f has a result"); + crate::resolve_type(&result, &fixture.resolve); + } + + #[test] + #[should_panic(expected = "implement a nested result in record field `r`")] + fn test_result_record_field_is_refused() { + analyze_types("record holder { r: result }"); + } + + #[test] + #[should_panic(expected = "implement a nested result in the payload of variant case `failed`")] + fn test_result_variant_payload_is_refused() { + analyze_types("variant outcome { done, failed(result) }"); + } + + #[test] + #[should_panic(expected = "implement a nested result in a list element")] + fn test_result_list_element_is_refused() { + resolve_export_result("list>"); + } + + #[test] + #[should_panic(expected = "implement a nested result in an option payload")] + fn test_result_option_payload_is_refused() { + resolve_export_result("option>"); + } + + #[test] + #[should_panic(expected = "implement a nested result in a result's ok type")] + fn test_result_ok_type_is_refused() { + resolve_export_result("result, string>"); + } + + /// `result<_, string>` is a single Go `error`, which a field could + /// declare, but gravity cannot lift or lower it there, so the nested + /// check refuses every result shape. + #[test] + #[should_panic(expected = "implement a nested result in record field `e`")] + fn test_error_only_result_record_field_is_refused() { + analyze_types("record holder { e: result<_, string> }"); + } + + /// A result in a function's own result is still supported. + #[test] + fn test_result_as_function_result_still_resolves() { + resolve_export_result("result"); + } } diff --git a/cmd/gravity/src/lib.rs b/cmd/gravity/src/lib.rs index 58d095c..34718bb 100644 --- a/cmd/gravity/src/lib.rs +++ b/cmd/gravity/src/lib.rs @@ -149,15 +149,21 @@ pub fn resolve_type(typ: &Type, resolve: &Resolve) -> GoType { // single pointer composes in every position (param, return, // record field, list element); the prior `(T, bool)` // comma-ok shape didn't. - TypeDefKind::Option(value) => { - GoType::Pointer(Box::new(resolve_type(value, resolve))) - } + TypeDefKind::Option(value) => GoType::Pointer(Box::new(resolve_nested_type( + value, + resolve, + "an option payload", + ))), // Various results, including specialised ones. TypeDefKind::Result(Result_ { ok: Some(ok), err: Some(Type::String), - }) => GoType::ValueOrError(Box::new(resolve_type(ok, resolve))), + }) => GoType::ValueOrError(Box::new(resolve_nested_type( + ok, + resolve, + "a result's ok type", + ))), TypeDefKind::Result(Result_ { ok: Some(_), err: Some(_), @@ -167,7 +173,7 @@ pub fn resolve_type(typ: &Type, resolve: &Resolve) -> GoType { TypeDefKind::Result(Result_ { ok: Some(ok), err: None, - }) => resolve_type(ok, resolve), + }) => resolve_nested_type(ok, resolve, "a result's ok type"), TypeDefKind::Result(Result_ { ok: None, err: Some(Type::String), @@ -181,7 +187,11 @@ pub fn resolve_type(typ: &Type, resolve: &Resolve) -> GoType { err: None, }) => GoType::Nothing, - TypeDefKind::List(inner) => GoType::Slice(Box::new(resolve_type(inner, resolve))), + TypeDefKind::List(inner) => GoType::Slice(Box::new(resolve_nested_type( + inner, + resolve, + "a list element", + ))), TypeDefKind::Future(_) => todo!("TODO(#4): implement future conversion"), TypeDefKind::Stream(_) => todo!("TODO(#4): implement stream conversion"), TypeDefKind::Type(_) => GoType::UserDefined(qualified_type_name(*id, resolve)), @@ -195,6 +205,25 @@ pub fn resolve_type(typ: &Type, resolve: &Resolve) -> GoType { } } +/// Resolves a WIT type that sits inside another type, such as a record field +/// or a list element. `position` names where it sits, for the error message. +/// +/// gravity represents a `result` with Go's multiple return values (`T, +/// error`, or `error` alone), which only a function's results can carry, so a +/// nested `result` is refused here rather than written out as Go that does not +/// compile. +pub fn resolve_nested_type(typ: &Type, resolve: &Resolve, position: &str) -> GoType { + if let Type::Id(id) = typ + && matches!( + resolve.types[dealias(resolve, *id)].kind, + TypeDefKind::Result(_) + ) + { + todo!("TODO(#4): implement a nested result in {position}"); + } + resolve_type(typ, resolve) +} + /// Like [`resolve_type`], but downgrades a top-level Variant to /// `interface{}` so existing call sites can keep passing the variant /// payload through `any`-typed plumbing (rule config returns, generic