diff --git a/Cargo.lock b/Cargo.lock index 2a0cae2..c7b9068 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,14 @@ dependencies = [ "wit-component", ] +[[package]] +name = "example-enum-collisions" +version = "0.0.2" +dependencies = [ + "wit-bindgen", + "wit-component", +] + [[package]] name = "example-iface-method-returns-string" version = "0.0.2" @@ -204,6 +212,14 @@ dependencies = [ "wit-component", ] +[[package]] +name = "example-result-errors" +version = "0.0.2" +dependencies = [ + "wit-bindgen", + "wit-component", +] + [[package]] name = "example-variants" version = "0.0.2" diff --git a/cmd/gravity/src/codegen/bindings.rs b/cmd/gravity/src/codegen/bindings.rs index 642de34..7816c2e 100644 --- a/cmd/gravity/src/codegen/bindings.rs +++ b/cmd/gravity/src/codegen/bindings.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use genco::{prelude::*, tokens::Tokens}; -use wit_bindgen_core::wit_parser::{Resolve, SizeAlign, World}; +use wit_bindgen_core::wit_parser::{Resolve, Result_, SizeAlign, Type, TypeDefKind, World}; use crate::{ codegen::{ @@ -77,10 +77,20 @@ impl<'a> Bindings<'a> { analyzed_imports: &AnalyzedImports, import_chains: BTreeMap>, ) { + let typed_result_errors = self.resolve.types.iter().any(|(_, def)| { + matches!( + def.kind, + TypeDefKind::Result(Result_ { + err: Some(err), + .. + }) if err != Type::String + ) + }); let config = FactoryConfig { analyzed_imports, import_chains, wasm_var_name: &self.raw_wasm_var, + typed_result_errors, }; FactoryGenerator::new(config).format_into(&mut self.out) } diff --git a/cmd/gravity/src/codegen/exports.rs b/cmd/gravity/src/codegen/exports.rs index 12e95c9..23178cd 100644 --- a/cmd/gravity/src/codegen/exports.rs +++ b/cmd/gravity/src/codegen/exports.rs @@ -246,4 +246,33 @@ mod tests { "the wasm export must be called with the param area pointer, got:\n{generated}" ); } + + /// Regression test: an export returning `result` with a non-string + /// E (here an enum) returns the err case as a `*ResultError[E]`. Gravity + /// used to panic generating it: "TODO(#4): implement remaining result + /// conversion". + #[test] + fn test_export_result_with_enum_err() { + let fixture = Fixture::parse( + "package test:fixture; + world test-world { + enum refusal { no-credential, malformed, untyped-peer } + record decided { name: string } + export decide: func(n: u32) -> result; + export only-err: func(n: u32) -> result<_, refusal>; + }", + ); + let decide = generate(&fixture, "decide"); + assert!(decide.contains(") (Decided, error) {"), "got:\n{decide}"); + assert!( + decide.contains("= &ResultError[Refusal]{Value: enum"), + "the err case must be a *ResultError[Refusal], got:\n{decide}" + ); + let only_err = generate(&fixture, "only-err"); + assert!(only_err.contains(") error {"), "got:\n{only_err}"); + assert!( + only_err.contains("= &ResultError[Refusal]{Value: enum"), + "got:\n{only_err}" + ); + } } diff --git a/cmd/gravity/src/codegen/factory.rs b/cmd/gravity/src/codegen/factory.rs index 7efa6ab..bf2fb69 100644 --- a/cmd/gravity/src/codegen/factory.rs +++ b/cmd/gravity/src/codegen/factory.rs @@ -7,7 +7,7 @@ use crate::{ go::{ GoIdentifier, comment, imports::{ - CONTEXT_CONTEXT, ERRORS_NEW, WAZERO_API_MEMORY, WAZERO_API_MODULE, + CONTEXT_CONTEXT, ERRORS_NEW, FMT_SPRINTF, WAZERO_API_MEMORY, WAZERO_API_MODULE, WAZERO_COMPILED_MODULE, WAZERO_NEW_MODULE_CONFIG, WAZERO_NEW_RUNTIME, WAZERO_RUNTIME, }, }, @@ -18,6 +18,9 @@ pub struct FactoryConfig<'a> { pub analyzed_imports: &'a AnalyzedImports, pub import_chains: BTreeMap>, pub wasm_var_name: &'a GoIdentifier, + /// Whether any function returns a `result` whose err type is not a + /// string, so the bindings need `ResultError`. + pub typed_result_errors: bool, } /// Generator for factory and instance types @@ -69,6 +72,26 @@ impl<'a> FactoryGenerator<'a> { }; } + /// Generate `ResultError`, the error a function returns for the err case + /// of a `result` whose E is not a string. + fn generate_result_error(&self, tokens: &mut Tokens) { + quote_in! { *tokens => + $(comment(&[ + "ResultError is the error a function returns for the err case of a WIT", + "result whose E is not a string; Value is the E. Recover it with", + "errors.As. Any other error is a trap or a host failure, never an E.", + ])) + type ResultError[E any] struct { + Value E + } + $['\n'] + func (e *ResultError[E]) Error() string { + return $FMT_SPRINTF("%v", e.Value) + } + $['\n'] + }; + } + /// Generate the Factory struct, constructor, and methods. fn generate_factory(&self, tokens: &mut Tokens) { let AnalyzedImports { @@ -168,12 +191,16 @@ impl<'a> FormatInto for &FactoryGenerator<'a> { tokens.push(); self.generate_write_string(tokens); tokens.push(); + if self.config.typed_result_errors { + self.generate_result_error(tokens); + tokens.push(); + } } } #[cfg(test)] mod tests { - use genco::lang::go::Tokens; + use genco::{lang::go::Tokens, tokens::FormatInto}; use crate::{ codegen::{FactoryGenerator, factory::FactoryConfig, ir::AnalyzedImports}, @@ -194,6 +221,7 @@ mod tests { analyzed_imports, import_chains: Default::default(), wasm_var_name: &GoIdentifier::public("test-wasm"), + typed_result_errors: false, }; let generator = FactoryGenerator::new(config); let mut tokens = Tokens::new(); @@ -201,4 +229,34 @@ mod tests { assert!(tokens.to_string().unwrap().contains("func writeString")); } + + /// `ResultError` is emitted exactly when a function needs it. + #[test] + fn test_result_error_only_when_needed() { + let analyzed_imports = &AnalyzedImports { + interfaces: vec![], + standalone_types: vec![], + standalone_functions: vec![], + factory_name: GoIdentifier::public("test-factory"), + instance_name: GoIdentifier::public("test-instance"), + constructor_name: GoIdentifier::public("test-constructor"), + }; + for typed_result_errors in [false, true] { + let config = FactoryConfig { + analyzed_imports, + import_chains: Default::default(), + wasm_var_name: &GoIdentifier::public("test-wasm"), + typed_result_errors, + }; + let generator = FactoryGenerator::new(config); + let mut tokens = Tokens::new(); + (&generator).format_into(&mut tokens); + let generated = tokens.to_string().unwrap(); + assert_eq!( + generated.contains("type ResultError[E any] struct"), + typed_result_errors, + "got:\n{generated}" + ); + } + } } diff --git a/cmd/gravity/src/codegen/func.rs b/cmd/gravity/src/codegen/func.rs index 06ad37b..0bc63b5 100644 --- a/cmd/gravity/src/codegen/func.rs +++ b/cmd/gravity/src/codegen/func.rs @@ -10,7 +10,7 @@ use crate::{ go::{ GoIdentifier, GoResult, GoType, Operand, comment, imports::{ - ERRORS_NEW, UTF8_VALID_RUNE, WAZERO_API_DECODE_F32, WAZERO_API_DECODE_F64, + ERRORS_AS, ERRORS_NEW, UTF8_VALID_RUNE, WAZERO_API_DECODE_F32, WAZERO_API_DECODE_F64, WAZERO_API_ENCODE_F32, WAZERO_API_ENCODE_F64, }, }, @@ -606,6 +606,69 @@ impl Bindgen for Func<'_> { results.push(Operand::SingleValue(err.into())); } + // A result whose err case is not a string: the err payload is + // returned as a `*ResultError[E]`, so `errors.As` recovers it and a + // trap (any other error) stays distinguishable. + Instruction::ResultLift { + result: + Result_ { + ok, + err: Some(err_typ), + }, + .. + } => { + let err_go = resolve_type(err_typ, resolve); + let (err_block, err_results) = self.pop_block(); + assert_eq!(err_results.len(), 1); + let err_op = &err_results[0]; + + let (ok_block, ok_results) = self.pop_block(); + + let tmp = self.tmp(); + let value = &format!("value{tmp}"); + let err = &format!("err{tmp}"); + let tag = &operands[0]; + match ok { + Some(ok_typ) => { + assert_eq!(ok_results.len(), 1); + let ok_op = &ok_results[0]; + let ok_go = resolve_type(ok_typ, resolve); + quote_in! { self.body => + $['\r'] + var $value $ok_go + var $err error + switch $tag { + case 0: + $ok_block + $value = $ok_op + case 1: + $err_block + $err = &ResultError[$err_go]{Value: $err_op} + default: + $err = $ERRORS_NEW("invalid variant discriminant for expected") + } + }; + results.push(Operand::MultiValue((value.into(), err.into()))); + } + None => { + assert_eq!(ok_results.len(), 0); + quote_in! { self.body => + $['\r'] + var $err error + switch $tag { + case 0: + $ok_block + case 1: + $err_block + $err = &ResultError[$err_go]{Value: $err_op} + default: + $err = $ERRORS_NEW("invalid variant discriminant for expected") + } + }; + results.push(Operand::SingleValue(err.into())); + } + } + } Instruction::ResultLift { .. } => todo!("implement instruction: {inst:?}"), Instruction::Return { amt, .. } => { if *amt != 0 { @@ -767,6 +830,53 @@ impl Bindgen for Func<'_> { } }; } + // The host returns a `*ResultError[E]` for the err case of a + // result whose E is not a string. Any other error has no WIT + // representation, so it traps the guest, as a panic in a host + // function does. + Instruction::ResultLower { + result: + Result_ { + ok, + err: Some(err_typ), + }, + .. + } => { + let err_go = resolve_type(err_typ, resolve); + let (err_block, _) = self.pop_block(); + let (ok_block, _) = self.pop_block(); + let tmp = self.tmp(); + let typed = &format!("typed{tmp}"); + let (ok_value, err) = match (ok, &operands[0]) { + (Some(_), Operand::MultiValue((ok_value, err))) => { + (Some(ok_value.clone()), err.clone()) + } + (None, Operand::SingleValue(err)) => (None, err.clone()), + (_, operand) => panic!( + "impossible: a result lowers from (value, error) or error, got {operand:?}" + ), + }; + let ok_arm = match &ok_value { + Some(ok_value) => quote! { + variantPayload := $ok_value + $ok_block + }, + None => quote!($ok_block), + }; + quote_in! { self.body => + $['\r'] + if $(&err) != nil { + var $typed *ResultError[$err_go] + if !$ERRORS_AS($(&err), &$typed) { + panic($(&err)) + } + variantPayload := $typed.Value + $err_block + } else { + $ok_arm + } + }; + } Instruction::ResultLower { .. } => todo!("implement instruction: {inst:?}"), Instruction::OptionLift { payload, .. } => { let (some, some_results) = self.blocks.pop().unwrap(); @@ -1048,14 +1158,15 @@ impl Bindgen for Func<'_> { } } } - Instruction::EnumLower { enum_, .. } => { + Instruction::EnumLower { enum_, ty, .. } => { + let name = crate::qualified_type_name(*ty, resolve); let value = &operands[0]; let tmp = self.tmp(); let enum_tmp = &format!("enum{tmp}"); let mut cases: Tokens = Tokens::new(); for (i, case) in enum_.cases.iter().enumerate() { - let case_name = GoIdentifier::public(case.name.clone()); + let case_name = GoIdentifier::public(crate::enum_case_name(&name, &case.name)); quote_in! { cases => $['\r'] case $case_name: @@ -1498,7 +1609,7 @@ impl Bindgen for Func<'_> { let mut cases: Tokens = Tokens::new(); for (i, case) in enum_.cases.iter().enumerate() { - let case_name = GoIdentifier::public(case.name.clone()); + let case_name = GoIdentifier::public(crate::enum_case_name(&name, &case.name)); quote_in! { cases => $['\r'] case $i: diff --git a/cmd/gravity/src/codegen/imports.rs b/cmd/gravity/src/codegen/imports.rs index f3d28b9..e5ae80f 100644 --- a/cmd/gravity/src/codegen/imports.rs +++ b/cmd/gravity/src/codegen/imports.rs @@ -19,7 +19,7 @@ use crate::{ }, go::{ GoIdentifier, GoResult, GoType, - imports::{CONTEXT_CONTEXT, WAZERO_API_MODULE}, + imports::{CONTEXT_CONTEXT, FMT_SPRINTF, WAZERO_API_MODULE}, }, resolve_param_type, resolve_type, resolve_wasm_type, }; @@ -381,7 +381,19 @@ impl<'a> ImportCodeGenerator<'a> { let enum_type = &GoIdentifier::private(&typ.name); let enum_interface = &typ.go_type_name; let enum_function = &GoIdentifier::private(format!("is-{}", &typ.name)); - let variants = cases.iter().map(GoIdentifier::public); + let variants = cases + .iter() + .map(|case| GoIdentifier::public(crate::enum_case_name(&typ.name, case))) + .collect::>(); + // String answers the WIT spelling of the case, so a case + // prints, and a ResultError carrying one reads, as WIT wrote it. + let names = variants.iter().zip(cases).map(|(name, case)| { + quote! { + case $name: + return $(quoted(case)) + } + }); + let unknown = format!("{}(%d)", typ.name); quote_in! { *tokens => $['\n'] type $(enum_interface) interface { @@ -393,9 +405,16 @@ impl<'a> ImportCodeGenerator<'a> { func ($(enum_type)) $enum_function() {} $['\n'] const ( - $(for name in variants join ($['\r']) => $name $enum_type = iota) + $(for name in &variants join ($['\r']) => $name $enum_type = iota) ) $['\n'] + func (e $(enum_type)) String() string { + switch e { + $(for arm in names join ($['\r']) => $arm) + } + return $FMT_SPRINTF($(quoted(unknown)), int(e)) + } + $['\n'] } } TypeDefinition::Alias { target } => { @@ -940,4 +959,109 @@ mod tests { other => panic!("alias analyzed as: {other:?}"), } } + + /// Regression test: enum case constants are named for their enum. WIT + /// scopes a case to its enum, and unqualified constants made two enums + /// sharing a case name, or a case sharing a type's name, redeclare an + /// identifier. + #[test] + fn test_enum_case_constants_are_named_for_their_enum() { + let fixture = Fixture::parse( + "package test:fixture; + interface types { + enum via { peer-svid, anonymous } + enum credential-kind { peer-svid, bearer } + } + world test-world { + import types; + }", + ); + let analyzed = ImportAnalyzer::new(&fixture.resolve, fixture.world()).analyze(); + let generator = ImportCodeGenerator::new(&fixture.resolve, &analyzed, &fixture.sizes); + let mut tokens = Tokens::new(); + generator.format_into(&mut tokens); + let generated = tokens.to_string().unwrap(); + + for constant in [ + "ViaPeerSvid via = iota", + "ViaAnonymous via = iota", + "CredentialKindPeerSvid credentialKind = iota", + "CredentialKindBearer credentialKind = iota", + ] { + assert!( + generated.contains(constant), + "expected `{constant}`, got:\n{generated}" + ); + } + assert!( + !generated.contains("\n PeerSvid "), + "no case constant may be unqualified, got:\n{generated}" + ); + } + + /// Regression test: a host import returning `result` with a + /// non-string E recovers the E from the host's error with `errors.As`, + /// and traps on any other error. + #[test] + fn test_import_result_with_record_err() { + let fixture = host_fixture( + "record problem { code: u32 } + probe: func(n: u32) -> result;", + ); + let method = InterfaceMethod { + name: "probe".to_string(), + go_method_name: GoIdentifier::public("Probe"), + parameters: vec![], + return_type: None, + wit_function: fixture.function("host", "probe").clone(), + }; + + let code_str = host_function(&fixture, &method); + assert!( + code_str.contains("*ResultError[Problem]"), + "expected a *ResultError[Problem] target, got:\n{code_str}" + ); + assert!( + code_str.contains("errors.As("), + "expected errors.As to recover the err case, got:\n{code_str}" + ); + assert!( + code_str.contains("variantPayload := typed"), + "the err payload must be the recovered E, got:\n{code_str}" + ); + } + + /// An enum's private type answers its WIT case name from String. + #[test] + fn test_enum_string_answers_the_wit_case() { + let fixture = Fixture::parse( + "package test:fixture; + interface types { + enum refusal { no-credential, untyped-peer } + } + world test-world { + import types; + }", + ); + let analyzed = ImportAnalyzer::new(&fixture.resolve, fixture.world()).analyze(); + let generator = ImportCodeGenerator::new(&fixture.resolve, &analyzed, &fixture.sizes); + let mut tokens = Tokens::new(); + generator.format_into(&mut tokens); + let generated = tokens.to_string().unwrap(); + + assert!( + generated.contains("func (e refusal) String() string {"), + "got:\n{generated}" + ); + for expected in [ + "case RefusalUntypedPeer:", + "return \"untyped-peer\"", + "\"refusal(%d)\"", + ] { + assert!( + generated.contains(expected), + "expected `{expected}`, got:\n{generated}" + ); + } + } } diff --git a/cmd/gravity/src/go/imports.rs b/cmd/gravity/src/go/imports.rs index 94522d5..c555911 100644 --- a/cmd/gravity/src/go/imports.rs +++ b/cmd/gravity/src/go/imports.rs @@ -11,6 +11,8 @@ impl FormatInto for GoImport { pub static CONTEXT_CONTEXT: GoImport = GoImport("context", "Context"); pub static ERRORS_NEW: GoImport = GoImport("errors", "New"); +pub static ERRORS_AS: GoImport = GoImport("errors", "As"); +pub static FMT_SPRINTF: GoImport = GoImport("fmt", "Sprintf"); pub static FMT_PRINTF: GoImport = GoImport("fmt", "Printf"); pub static WAZERO_RUNTIME: GoImport = GoImport("github.com/tetratelabs/wazero", "Runtime"); pub static WAZERO_NEW_RUNTIME: GoImport = GoImport("github.com/tetratelabs/wazero", "NewRuntime"); diff --git a/cmd/gravity/src/lib.rs b/cmd/gravity/src/lib.rs index 58d095c..1a3ccdb 100644 --- a/cmd/gravity/src/lib.rs +++ b/cmd/gravity/src/lib.rs @@ -91,6 +91,19 @@ pub fn qualified_type_name(type_id: TypeId, resolve: &Resolve) -> String { } } +/// The kebab-case name of the Go constant for case `case` of the enum +/// named `enum_name` (its `qualified_type_name`): the enum's name, then the +/// case's. +/// +/// Every case is namespaced by its enum, as Go's own convention spells an +/// enumerated constant (`http.MethodGet`). WIT scopes a case name to its +/// enum, so two enums may share one (`via { peer-svid }` and +/// `credential-kind { peer-svid }`), and a case may share a type's name; an +/// unqualified constant redeclared the first and shadowed the second. +pub fn enum_case_name(enum_name: &str, case: &str) -> String { + format!("{enum_name}-{case}") +} + /// Resolves a Wasm type to a Go type. pub fn resolve_wasm_type(typ: &WasmType) -> GoType { match typ { @@ -158,12 +171,11 @@ pub fn resolve_type(typ: &Type, resolve: &Resolve) -> GoType { ok: Some(ok), err: Some(Type::String), }) => GoType::ValueOrError(Box::new(resolve_type(ok, resolve))), + // Any other err type comes back as a `*ResultError[E]`. TypeDefKind::Result(Result_ { - ok: Some(_), + ok: Some(ok), err: Some(_), - }) => { - todo!("TODO(#4): implement remaining result conversion") - } + }) => GoType::ValueOrError(Box::new(resolve_type(ok, resolve))), TypeDefKind::Result(Result_ { ok: Some(ok), err: None, @@ -175,7 +187,7 @@ pub fn resolve_type(typ: &Type, resolve: &Resolve) -> GoType { TypeDefKind::Result(Result_ { ok: None, err: Some(_), - }) => todo!("TODO(#4): implement remaining result conversion"), + }) => GoType::Error, TypeDefKind::Result(Result_ { ok: None, err: None, diff --git a/cmd/gravity/tests/cmd/enum-collisions.stderr b/cmd/gravity/tests/cmd/enum-collisions.stderr new file mode 100644 index 0000000..e69de29 diff --git a/cmd/gravity/tests/cmd/enum-collisions.stdout b/cmd/gravity/tests/cmd/enum-collisions.stdout new file mode 100644 index 0000000..1c13d42 --- /dev/null +++ b/cmd/gravity/tests/cmd/enum-collisions.stdout @@ -0,0 +1,286 @@ +// Code generated by arcjet-gravity; DO NOT EDIT. + +package enum_collisions + +import "context" +import "errors" +import "fmt" +import "github.com/tetratelabs/wazero" +import "github.com/tetratelabs/wazero/api" + +import _ "embed" + +//go:embed enum_collisions.wasm +var wasmFileEnumCollisions []byte + +type IEnumCollisionsTypes interface {} + +type Via interface { + isVia() +} + +type via int + +func (via) isVia() {} + +const ( + ViaPeerSvid via = iota + ViaSignedToken via = iota + ViaAnonymous via = iota +) + +func (e via) String() string { + switch e { + case ViaPeerSvid: + return "peer-svid" + case ViaSignedToken: + return "signed-token" + case ViaAnonymous: + return "anonymous" + } + return fmt.Sprintf("via(%d)", int(e)) +} + +type CredentialKind interface { + isCredentialKind() +} + +type credentialKind int + +func (credentialKind) isCredentialKind() {} + +const ( + CredentialKindPeerSvid credentialKind = iota + CredentialKindSignedToken credentialKind = iota + CredentialKindBearer credentialKind = iota +) + +func (e credentialKind) String() string { + switch e { + case CredentialKindPeerSvid: + return "peer-svid" + case CredentialKindSignedToken: + return "signed-token" + case CredentialKindBearer: + return "bearer" + } + return fmt.Sprintf("credential-kind(%d)", int(e)) +} + +type Point struct { + X int32 + Y int32 +} + +type Shape interface { + isShape() +} + +type shape int + +func (shape) isShape() {} + +const ( + ShapePoint shape = iota + ShapeLine shape = iota +) + +func (e shape) String() string { + switch e { + case ShapePoint: + return "point" + case ShapeLine: + return "line" + } + return fmt.Sprintf("shape(%d)", int(e)) +} + +type EnumCollisionsFactory struct { + runtime wazero.Runtime + module wazero.CompiledModule +} + +func NewEnumCollisionsFactory( + ctx context.Context, + types IEnumCollisionsTypes, +) (*EnumCollisionsFactory, error) { + wazeroRuntime := wazero.NewRuntime(ctx) + + _, err0 := wazeroRuntime.NewHostModuleBuilder("gravity:enum-collisions/types"). + Instantiate(ctx) + if err0 != nil { + return nil, err0 + } + + // Compiling the module takes a LONG time, so we want to do it once and hold + // onto it with the Runtime + module, err := wazeroRuntime.CompileModule(ctx, wasmFileEnumCollisions) + if err != nil { + return nil, err + } + return &EnumCollisionsFactory{ + runtime: wazeroRuntime, + module: module, + }, nil +} + +func (f *EnumCollisionsFactory) Instantiate(ctx context.Context) (*EnumCollisionsInstance, error) { + if module, err := f.runtime.InstantiateModule(ctx, f.module, wazero.NewModuleConfig()); err != nil { + return nil, err + } else { + return &EnumCollisionsInstance{module}, nil + } +} + +func (f *EnumCollisionsFactory) Close(ctx context.Context) { + f.runtime.Close(ctx) +} + +type EnumCollisionsInstance struct { + module api.Module +} + +func (i *EnumCollisionsInstance) Close(ctx context.Context) error { + if err := i.module.Close(ctx); err != nil { + return err + } + + return nil +} + +// writeString will put a Go string into the Wasm memory following the Component +// Model calling conventions, such as allocating memory with the realloc function +func writeString( + ctx context.Context, + s string, + memory api.Memory, + realloc api.Function, +) (uint64, uint64, error) { + if len(s) == 0 { + return 1, 0, nil + } + + results, err := realloc.Call(ctx, 0, 0, 1, uint64(len(s))) + if err != nil { + return 1, 0, err + } + ptr := results[0] + ok := memory.Write(uint32(ptr), []byte(s)) + if !ok { + return 1, 0, errors.New("failed to write string to wasm memory") + } + return uint64(ptr), uint64(len(s)), nil +} + +func (i *EnumCollisionsInstance) ViaFor( + ctx context.Context, + k CredentialKind, +) Via { + arg0 := k + var enum0 uint32 + switch arg0 { + case CredentialKindPeerSvid: + enum0 = 0 + case CredentialKindSignedToken: + enum0 = 1 + case CredentialKindBearer: + enum0 = 2 + default: + panic(errors.New("invalid enum type provided")) + } + raw1, err1 := i.module.ExportedFunction("via-for").Call(ctx, uint64(enum0)) + // The return type doesn't contain an error so we panic if one is encountered + if err1 != nil { + panic(err1) + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_via-for"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + var enum2 Via + switch results1 { + case 0: + enum2 = ViaPeerSvid + case 1: + enum2 = ViaSignedToken + case 2: + enum2 = ViaAnonymous + default: + // The return type doesn't contain an error so we panic if one is encountered + panic(errors.New("invalid via discriminant")) + } + return enum2 +} + +func (i *EnumCollisionsInstance) Describe( + ctx context.Context, + s Shape, + p Point, +) string { + arg0 := s + arg1 := p + var enum0 uint32 + switch arg0 { + case ShapePoint: + enum0 = 0 + case ShapeLine: + enum0 = 1 + default: + panic(errors.New("invalid enum type provided")) + } + x1 := arg1.X + y1 := arg1.Y + value2 := uint32(x1) + value3 := uint32(y1) + raw4, err4 := i.module.ExportedFunction("describe").Call(ctx, uint64(enum0), uint64(value2), uint64(value3)) + // The return type doesn't contain an error so we panic if one is encountered + if err4 != nil { + panic(err4) + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_describe"); postFn != nil { + if _, err := postFn.Call(ctx, raw4...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results4 := raw4[0] + ptr5, ok5 := i.module.Memory().ReadUint32Le(uint32(results4 + 0)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok5 { + panic(errors.New("failed to read pointer from memory")) + } + len6, ok6 := i.module.Memory().ReadUint32Le(uint32(results4 + 4)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok6 { + panic(errors.New("failed to read length from memory")) + } + buf7, ok7 := i.module.Memory().Read(ptr5, len6) + // The return type doesn't contain an error so we panic if one is encountered + if !ok7 { + panic(errors.New("failed to read bytes from memory")) + } + str7 := string(buf7) + return str7 +} + diff --git a/cmd/gravity/tests/cmd/enum-collisions.toml b/cmd/gravity/tests/cmd/enum-collisions.toml new file mode 100644 index 0000000..eadc32b --- /dev/null +++ b/cmd/gravity/tests/cmd/enum-collisions.toml @@ -0,0 +1,2 @@ +bin.name = "gravity" +args = "--world enum-collisions ../../target/wasm32-unknown-unknown/release/example_enum_collisions.wasm" diff --git a/cmd/gravity/tests/cmd/instructions.stdout b/cmd/gravity/tests/cmd/instructions.stdout index 02e9465..0c66efb 100644 --- a/cmd/gravity/tests/cmd/instructions.stdout +++ b/cmd/gravity/tests/cmd/instructions.stdout @@ -4,6 +4,7 @@ package instructions import "context" import "errors" +import "fmt" import "github.com/tetratelabs/wazero" import "github.com/tetratelabs/wazero/api" @@ -21,11 +22,23 @@ type enumValues int func (enumValues) isEnumValues() {} const ( - One enumValues = iota - Two enumValues = iota - Three enumValues = iota + EnumValuesOne enumValues = iota + EnumValuesTwo enumValues = iota + EnumValuesThree enumValues = iota ) +func (e enumValues) String() string { + switch e { + case EnumValuesOne: + return "one" + case EnumValuesTwo: + return "two" + case EnumValuesThree: + return "three" + } + return fmt.Sprintf("enum-values(%d)", int(e)) +} + type WideParams struct { A uint32 B uint32 @@ -258,11 +271,11 @@ func (i *InstructionsInstance) EnumInput( arg0 := val var enum0 uint32 switch arg0 { - case One: + case EnumValuesOne: enum0 = 0 - case Two: + case EnumValuesTwo: enum0 = 1 - case Three: + case EnumValuesThree: enum0 = 2 default: panic(errors.New("invalid enum type provided")) diff --git a/cmd/gravity/tests/cmd/memory.stdout b/cmd/gravity/tests/cmd/memory.stdout index 0cbd7e2..9e2cac6 100644 --- a/cmd/gravity/tests/cmd/memory.stdout +++ b/cmd/gravity/tests/cmd/memory.stdout @@ -4,6 +4,7 @@ package memory import "context" import "errors" +import "fmt" import "github.com/tetratelabs/wazero" import "github.com/tetratelabs/wazero/api" import "unicode/utf8" @@ -29,11 +30,23 @@ type color int func (color) isColor() {} const ( - Red color = iota - Green color = iota - Blue color = iota + ColorRed color = iota + ColorGreen color = iota + ColorBlue color = iota ) +func (e color) String() string { + switch e { + case ColorRed: + return "red" + case ColorGreen: + return "green" + case ColorBlue: + return "blue" + } + return fmt.Sprintf("color(%d)", int(e)) +} + type Everything struct { AU8 uint8 AS8 int8 @@ -168,11 +181,11 @@ func NewMemoryFactory( var enum23 Color switch value22 { case 0: - enum23 = Red + enum23 = ColorRed case 1: - enum23 = Green + enum23 = ColorGreen case 2: - enum23 = Blue + enum23 = ColorBlue default: // The return type doesn't contain an error so we panic if one is encountered panic(errors.New("invalid color discriminant")) @@ -276,11 +289,11 @@ func NewMemoryFactory( mod.Memory().WriteUint32Le(arg1+44, uint32(value46)) var enum47 uint32 switch aColor35 { - case Red: + case ColorRed: enum47 = 0 - case Green: + case ColorGreen: enum47 = 1 - case Blue: + case ColorBlue: enum47 = 2 default: panic(errors.New("invalid enum type provided")) @@ -427,11 +440,11 @@ func (i *MemoryInstance) RoundTrip( i.module.Memory().WriteUint32Le(ptr0+44, uint32(value12)) var enum13 uint32 switch aColor1 { - case Red: + case ColorRed: enum13 = 0 - case Green: + case ColorGreen: enum13 = 1 - case Blue: + case ColorBlue: enum13 = 2 default: panic(errors.New("invalid enum type provided")) @@ -555,11 +568,11 @@ func (i *MemoryInstance) RoundTrip( var enum42 Color switch value41 { case 0: - enum42 = Red + enum42 = ColorRed case 1: - enum42 = Green + enum42 = ColorGreen case 2: - enum42 = Blue + enum42 = ColorBlue default: // The return type doesn't contain an error so we panic if one is encountered panic(errors.New("invalid color discriminant")) @@ -679,11 +692,11 @@ func (i *MemoryInstance) CallHostEcho( i.module.Memory().WriteUint32Le(ptr0+44, uint32(value12)) var enum13 uint32 switch aColor1 { - case Red: + case ColorRed: enum13 = 0 - case Green: + case ColorGreen: enum13 = 1 - case Blue: + case ColorBlue: enum13 = 2 default: panic(errors.New("invalid enum type provided")) @@ -807,11 +820,11 @@ func (i *MemoryInstance) CallHostEcho( var enum42 Color switch value41 { case 0: - enum42 = Red + enum42 = ColorRed case 1: - enum42 = Green + enum42 = ColorGreen case 2: - enum42 = Blue + enum42 = ColorBlue default: // The return type doesn't contain an error so we panic if one is encountered panic(errors.New("invalid color discriminant")) diff --git a/cmd/gravity/tests/cmd/regressions.stdout b/cmd/gravity/tests/cmd/regressions.stdout index 4893f1b..08967ad 100644 --- a/cmd/gravity/tests/cmd/regressions.stdout +++ b/cmd/gravity/tests/cmd/regressions.stdout @@ -4,6 +4,7 @@ package regressions import "context" import "errors" +import "fmt" import "github.com/tetratelabs/wazero" import "github.com/tetratelabs/wazero/api" @@ -32,11 +33,23 @@ type status int func (status) isStatus() {} const ( - Active status = iota - Inactive status = iota - Unknown status = iota + StatusActive status = iota + StatusInactive status = iota + StatusUnknown status = iota ) +func (e status) String() string { + switch e { + case StatusActive: + return "active" + case StatusInactive: + return "inactive" + case StatusUnknown: + return "unknown" + } + return fmt.Sprintf("status(%d)", int(e)) +} + type IRegressionsProcessor interface { Double( ctx context.Context, @@ -66,11 +79,23 @@ type emailCheckerValidatorResponse int func (emailCheckerValidatorResponse) isEmailCheckerValidatorResponse() {} const ( - Yes emailCheckerValidatorResponse = iota - No emailCheckerValidatorResponse = iota - Maybe emailCheckerValidatorResponse = iota + EmailCheckerValidatorResponseYes emailCheckerValidatorResponse = iota + EmailCheckerValidatorResponseNo emailCheckerValidatorResponse = iota + EmailCheckerValidatorResponseMaybe emailCheckerValidatorResponse = iota ) +func (e emailCheckerValidatorResponse) String() string { + switch e { + case EmailCheckerValidatorResponseYes: + return "yes" + case EmailCheckerValidatorResponseNo: + return "no" + case EmailCheckerValidatorResponseMaybe: + return "maybe" + } + return fmt.Sprintf("email-checker-validator-response(%d)", int(e)) +} + type IRegressionsBotVerifier interface { Verify( ctx context.Context, @@ -87,11 +112,23 @@ type botVerifierValidatorResponse int func (botVerifierValidatorResponse) isBotVerifierValidatorResponse() {} const ( - Verified botVerifierValidatorResponse = iota - Spoofed botVerifierValidatorResponse = iota - Unverifiable botVerifierValidatorResponse = iota + BotVerifierValidatorResponseVerified botVerifierValidatorResponse = iota + BotVerifierValidatorResponseSpoofed botVerifierValidatorResponse = iota + BotVerifierValidatorResponseUnverifiable botVerifierValidatorResponse = iota ) +func (e botVerifierValidatorResponse) String() string { + switch e { + case BotVerifierValidatorResponseVerified: + return "verified" + case BotVerifierValidatorResponseSpoofed: + return "spoofed" + case BotVerifierValidatorResponseUnverifiable: + return "unverifiable" + } + return fmt.Sprintf("bot-verifier-validator-response(%d)", int(e)) +} + type IRegressionsIpSource interface { Lookup( ctx context.Context, @@ -131,11 +168,11 @@ func NewRegressionsFactory( value1 := botVerifier.Verify(ctx, str0) var enum2 uint32 switch value1 { - case Verified: + case BotVerifierValidatorResponseVerified: enum2 = 0 - case Spoofed: + case BotVerifierValidatorResponseSpoofed: enum2 = 1 - case Unverifiable: + case BotVerifierValidatorResponseUnverifiable: enum2 = 2 default: panic(errors.New("invalid enum type provided")) @@ -185,11 +222,11 @@ func NewRegressionsFactory( value1 := checker.GetStatus(ctx, str0) var enum2 uint32 switch value1 { - case Active: + case StatusActive: enum2 = 0 - case Inactive: + case StatusInactive: enum2 = 1 - case Unknown: + case StatusUnknown: enum2 = 2 default: panic(errors.New("invalid enum type provided")) @@ -217,11 +254,11 @@ func NewRegressionsFactory( value1 := emailChecker.IsAllowed(ctx, str0) var enum2 uint32 switch value1 { - case Yes: + case EmailCheckerValidatorResponseYes: enum2 = 0 - case No: + case EmailCheckerValidatorResponseNo: enum2 = 1 - case Maybe: + case EmailCheckerValidatorResponseMaybe: enum2 = 2 default: panic(errors.New("invalid enum type provided")) diff --git a/cmd/gravity/tests/cmd/result-errors.stderr b/cmd/gravity/tests/cmd/result-errors.stderr new file mode 100644 index 0000000..e69de29 diff --git a/cmd/gravity/tests/cmd/result-errors.stdout b/cmd/gravity/tests/cmd/result-errors.stdout new file mode 100644 index 0000000..bfc0496 --- /dev/null +++ b/cmd/gravity/tests/cmd/result-errors.stdout @@ -0,0 +1,977 @@ +// Code generated by arcjet-gravity; DO NOT EDIT. + +package result_errors + +import "context" +import "errors" +import "fmt" +import "github.com/tetratelabs/wazero" +import "github.com/tetratelabs/wazero/api" + +import _ "embed" + +//go:embed result_errors.wasm +var wasmFileResultErrors []byte + +type IResultErrorsTypes interface {} + +type Refusal interface { + isRefusal() +} + +type refusal int + +func (refusal) isRefusal() {} + +const ( + RefusalNoCredential refusal = iota + RefusalMalformed refusal = iota + RefusalUntypedPeer refusal = iota + RefusalForeignDomain refusal = iota +) + +func (e refusal) String() string { + switch e { + case RefusalNoCredential: + return "no-credential" + case RefusalMalformed: + return "malformed" + case RefusalUntypedPeer: + return "untyped-peer" + case RefusalForeignDomain: + return "foreign-domain" + } + return fmt.Sprintf("refusal(%d)", int(e)) +} + +type Problem struct { + Code uint32 + Detail string +} + +type Fault interface { + isFault() +} + +type FaultRefused struct { + Value Refusal +} + +func (FaultRefused) isFault() {} + +func (Problem) isFault() {} + +type FaultMessage struct { + Value string +} + +func (FaultMessage) isFault() {} + +type FaultUnknown struct {} + +func (FaultUnknown) isFault() {} + +type Decided struct { + Name string + N uint32 +} + +type IResultErrorsHost interface { + Check( + ctx context.Context, + n uint32, + ) (uint32, error) + Probe( + ctx context.Context, + n uint32, + ) error + Explain( + ctx context.Context, + n uint32, + ) (string, error) +} + +type ResultErrorsFactory struct { + runtime wazero.Runtime + module wazero.CompiledModule +} + +func NewResultErrorsFactory( + ctx context.Context, + types IResultErrorsTypes, + host IResultErrorsHost, +) (*ResultErrorsFactory, error) { + wazeroRuntime := wazero.NewRuntime(ctx) + + _, err1 := wazeroRuntime.NewHostModuleBuilder("gravity:result-errors/host"). + NewFunctionBuilder(). + WithFunc(func( + ctx context.Context, + mod api.Module, + arg0 uint32, + arg1 uint32, + ) { + result0 := uint32(arg0) + value1, err1 := host.Check(ctx, result0) + if err1 != nil { + var typed4 *ResultError[Refusal] + if !errors.As(err1, &typed4) { + panic(err1) + } + variantPayload := typed4.Value + mod.Memory().WriteByte(arg1+0, uint8(1)) + var enum3 uint32 + switch variantPayload { + case RefusalNoCredential: + enum3 = 0 + case RefusalMalformed: + enum3 = 1 + case RefusalUntypedPeer: + enum3 = 2 + case RefusalForeignDomain: + enum3 = 3 + default: + panic(errors.New("invalid enum type provided")) + } + mod.Memory().WriteByte(arg1+4, uint8(enum3)) + } else { + variantPayload := value1 + mod.Memory().WriteByte(arg1+0, uint8(0)) + result2 := uint32(variantPayload) + mod.Memory().WriteUint32Le(arg1+4, uint32(result2)) + } + }). + Export("check"). + NewFunctionBuilder(). + WithFunc(func( + ctx context.Context, + mod api.Module, + arg0 uint32, + arg1 uint32, + ) { + result0 := uint32(arg0) + err1 := host.Probe(ctx, result0) + if err1 != nil { + var typed5 *ResultError[Problem] + if !errors.As(err1, &typed5) { + panic(err1) + } + variantPayload := typed5.Value + mod.Memory().WriteByte(arg1+0, uint8(1)) + code2 := variantPayload.Code + detail2 := variantPayload.Detail + result3 := uint32(code2) + mod.Memory().WriteUint32Le(arg1+4, uint32(result3)) + memory4 := mod.Memory() + realloc4 := mod.ExportedFunction("cabi_realloc") + ptr4, len4, err4 := writeString(ctx, detail2, memory4, realloc4) + if err4 != nil { + panic(err4) + } + mod.Memory().WriteUint32Le(arg1+12, uint32(len4)) + mod.Memory().WriteUint32Le(arg1+8, uint32(ptr4)) + } else { + mod.Memory().WriteByte(arg1+0, uint8(0)) + } + }). + Export("probe"). + NewFunctionBuilder(). + WithFunc(func( + ctx context.Context, + mod api.Module, + arg0 uint32, + arg1 uint32, + ) { + result0 := uint32(arg0) + value1, err1 := host.Explain(ctx, result0) + if err1 != nil { + var typed9 *ResultError[Fault] + if !errors.As(err1, &typed9) { + panic(err1) + } + variantPayload := typed9.Value + mod.Memory().WriteByte(arg1+0, uint8(1)) + switch case8 := variantPayload.(type) { + case FaultRefused: + variantPayload := case8.Value + mod.Memory().WriteByte(arg1+4, uint8(0)) + var enum3 uint32 + switch variantPayload { + case RefusalNoCredential: + enum3 = 0 + case RefusalMalformed: + enum3 = 1 + case RefusalUntypedPeer: + enum3 = 2 + case RefusalForeignDomain: + enum3 = 3 + default: + panic(errors.New("invalid enum type provided")) + } + mod.Memory().WriteByte(arg1+8, uint8(enum3)) + case Problem: + variantPayload := case8 + mod.Memory().WriteByte(arg1+4, uint8(1)) + code4 := variantPayload.Code + detail4 := variantPayload.Detail + result5 := uint32(code4) + mod.Memory().WriteUint32Le(arg1+8, uint32(result5)) + memory6 := mod.Memory() + realloc6 := mod.ExportedFunction("cabi_realloc") + ptr6, len6, err6 := writeString(ctx, detail4, memory6, realloc6) + if err6 != nil { + panic(err6) + } + mod.Memory().WriteUint32Le(arg1+16, uint32(len6)) + mod.Memory().WriteUint32Le(arg1+12, uint32(ptr6)) + case FaultMessage: + variantPayload := case8.Value + mod.Memory().WriteByte(arg1+4, uint8(2)) + memory7 := mod.Memory() + realloc7 := mod.ExportedFunction("cabi_realloc") + ptr7, len7, err7 := writeString(ctx, variantPayload, memory7, realloc7) + if err7 != nil { + panic(err7) + } + mod.Memory().WriteUint32Le(arg1+12, uint32(len7)) + mod.Memory().WriteUint32Le(arg1+8, uint32(ptr7)) + case FaultUnknown: + _ = case8 + mod.Memory().WriteByte(arg1+4, uint8(3)) + default: + // The return type doesn't contain an error so we panic if one is encountered + panic(errors.New("invalid variant type provided")) + } + } else { + variantPayload := value1 + mod.Memory().WriteByte(arg1+0, uint8(0)) + memory2 := mod.Memory() + realloc2 := mod.ExportedFunction("cabi_realloc") + ptr2, len2, err2 := writeString(ctx, variantPayload, memory2, realloc2) + if err2 != nil { + panic(err2) + } + mod.Memory().WriteUint32Le(arg1+8, uint32(len2)) + mod.Memory().WriteUint32Le(arg1+4, uint32(ptr2)) + } + }). + Export("explain"). + Instantiate(ctx) + if err1 != nil { + return nil, err1 + } + _, err0 := wazeroRuntime.NewHostModuleBuilder("gravity:result-errors/types"). + Instantiate(ctx) + if err0 != nil { + return nil, err0 + } + + // Compiling the module takes a LONG time, so we want to do it once and hold + // onto it with the Runtime + module, err := wazeroRuntime.CompileModule(ctx, wasmFileResultErrors) + if err != nil { + return nil, err + } + return &ResultErrorsFactory{ + runtime: wazeroRuntime, + module: module, + }, nil +} + +func (f *ResultErrorsFactory) Instantiate(ctx context.Context) (*ResultErrorsInstance, error) { + if module, err := f.runtime.InstantiateModule(ctx, f.module, wazero.NewModuleConfig()); err != nil { + return nil, err + } else { + return &ResultErrorsInstance{module}, nil + } +} + +func (f *ResultErrorsFactory) Close(ctx context.Context) { + f.runtime.Close(ctx) +} + +type ResultErrorsInstance struct { + module api.Module +} + +func (i *ResultErrorsInstance) Close(ctx context.Context) error { + if err := i.module.Close(ctx); err != nil { + return err + } + + return nil +} + +// writeString will put a Go string into the Wasm memory following the Component +// Model calling conventions, such as allocating memory with the realloc function +func writeString( + ctx context.Context, + s string, + memory api.Memory, + realloc api.Function, +) (uint64, uint64, error) { + if len(s) == 0 { + return 1, 0, nil + } + + results, err := realloc.Call(ctx, 0, 0, 1, uint64(len(s))) + if err != nil { + return 1, 0, err + } + ptr := results[0] + ok := memory.Write(uint32(ptr), []byte(s)) + if !ok { + return 1, 0, errors.New("failed to write string to wasm memory") + } + return uint64(ptr), uint64(len(s)), nil +} + +// ResultError is the error a function returns for the err case of a WIT +// result whose E is not a string; Value is the E. Recover it with +// errors.As. Any other error is a trap or a host failure, never an E. +type ResultError[E any] struct { + Value E +} + +func (e *ResultError[E]) Error() string { + return fmt.Sprintf("%v", e.Value) +} + +func (i *ResultErrorsInstance) Decide( + ctx context.Context, + n uint32, +) (Decided, error) { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("decide").Call(ctx, uint64(result0)) + if err1 != nil { + var default1 Decided + return default1, err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_decide"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + var default2 Decided + return default2, errors.New("failed to read byte from memory") + } + var value11 Decided + var err11 error + switch value2 { + case 0: + ptr3, ok3 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok3 { + var default3 Decided + return default3, errors.New("failed to read pointer from memory") + } + len4, ok4 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok4 { + var default4 Decided + return default4, errors.New("failed to read length from memory") + } + buf5, ok5 := i.module.Memory().Read(ptr3, len4) + if !ok5 { + var default5 Decided + return default5, errors.New("failed to read bytes from memory") + } + str5 := string(buf5) + value6, ok6 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok6 { + var default6 Decided + return default6, errors.New("failed to read i32 from memory") + } + result7 := uint32(value6) + value8 := Decided{ + Name: str5, + N: result7, + } + value11 = value8 + case 1: + value9, ok9 := i.module.Memory().ReadByte(uint32(results1 + 4)) + if !ok9 { + var default9 Decided + return default9, errors.New("failed to read byte from memory") + } + var enum10 Refusal + switch value9 { + case 0: + enum10 = RefusalNoCredential + case 1: + enum10 = RefusalMalformed + case 2: + enum10 = RefusalUntypedPeer + case 3: + enum10 = RefusalForeignDomain + default: + var default0 Decided + return default0, errors.New("invalid refusal discriminant") + } + err11 = &ResultError[Refusal]{Value: enum10} + default: + err11 = errors.New("invalid variant discriminant for expected") + } + return value11, err11 +} + +func (i *ResultErrorsInstance) Inspect( + ctx context.Context, + n uint32, +) (uint32, error) { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("inspect").Call(ctx, uint64(result0)) + if err1 != nil { + var default1 uint32 + return default1, err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_inspect"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + var default2 uint32 + return default2, errors.New("failed to read byte from memory") + } + var value11 uint32 + var err11 error + switch value2 { + case 0: + value3, ok3 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok3 { + var default3 uint32 + return default3, errors.New("failed to read i32 from memory") + } + result4 := uint32(value3) + value11 = result4 + case 1: + value5, ok5 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok5 { + var default5 uint32 + return default5, errors.New("failed to read i32 from memory") + } + result6 := uint32(value5) + ptr7, ok7 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok7 { + var default7 uint32 + return default7, errors.New("failed to read pointer from memory") + } + len8, ok8 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok8 { + var default8 uint32 + return default8, errors.New("failed to read length from memory") + } + buf9, ok9 := i.module.Memory().Read(ptr7, len8) + if !ok9 { + var default9 uint32 + return default9, errors.New("failed to read bytes from memory") + } + str9 := string(buf9) + value10 := Problem{ + Code: result6, + Detail: str9, + } + err11 = &ResultError[Problem]{Value: value10} + default: + err11 = errors.New("invalid variant discriminant for expected") + } + return value11, err11 +} + +func (i *ResultErrorsInstance) Classify( + ctx context.Context, + n uint32, +) (string, error) { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("classify").Call(ctx, uint64(result0)) + if err1 != nil { + var default1 string + return default1, err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_classify"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + var default2 string + return default2, errors.New("failed to read byte from memory") + } + var value19 string + var err19 error + switch value2 { + case 0: + ptr3, ok3 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok3 { + var default3 string + return default3, errors.New("failed to read pointer from memory") + } + len4, ok4 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok4 { + var default4 string + return default4, errors.New("failed to read length from memory") + } + buf5, ok5 := i.module.Memory().Read(ptr3, len4) + if !ok5 { + var default5 string + return default5, errors.New("failed to read bytes from memory") + } + str5 := string(buf5) + value19 = str5 + case 1: + value6, ok6 := i.module.Memory().ReadByte(uint32(results1 + 4)) + if !ok6 { + var default6 string + return default6, errors.New("failed to read byte from memory") + } + var value18 Fault + switch value6 { + case 0: + value7, ok7 := i.module.Memory().ReadByte(uint32(results1 + 8)) + if !ok7 { + var default7 string + return default7, errors.New("failed to read byte from memory") + } + var enum8 Refusal + switch value7 { + case 0: + enum8 = RefusalNoCredential + case 1: + enum8 = RefusalMalformed + case 2: + enum8 = RefusalUntypedPeer + case 3: + enum8 = RefusalForeignDomain + default: + var default0 string + return default0, errors.New("invalid refusal discriminant") + } + value18 = FaultRefused{Value: enum8} + case 1: + value9, ok9 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok9 { + var default9 string + return default9, errors.New("failed to read i32 from memory") + } + result10 := uint32(value9) + ptr11, ok11 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok11 { + var default11 string + return default11, errors.New("failed to read pointer from memory") + } + len12, ok12 := i.module.Memory().ReadUint32Le(uint32(results1 + 16)) + if !ok12 { + var default12 string + return default12, errors.New("failed to read length from memory") + } + buf13, ok13 := i.module.Memory().Read(ptr11, len12) + if !ok13 { + var default13 string + return default13, errors.New("failed to read bytes from memory") + } + str13 := string(buf13) + value14 := Problem{ + Code: result10, + Detail: str13, + } + value18 = value14 + case 2: + ptr15, ok15 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok15 { + var default15 string + return default15, errors.New("failed to read pointer from memory") + } + len16, ok16 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok16 { + var default16 string + return default16, errors.New("failed to read length from memory") + } + buf17, ok17 := i.module.Memory().Read(ptr15, len16) + if !ok17 { + var default17 string + return default17, errors.New("failed to read bytes from memory") + } + str17 := string(buf17) + value18 = FaultMessage{Value: str17} + case 3: + value18 = FaultUnknown{} + default: + var default0 string + return default0, errors.New("invalid fault discriminant") + } + err19 = &ResultError[Fault]{Value: value18} + default: + err19 = errors.New("invalid variant discriminant for expected") + } + return value19, err19 +} + +func (i *ResultErrorsInstance) OnlyErr( + ctx context.Context, + n uint32, +) error { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("only-err").Call(ctx, uint64(result0)) + if err1 != nil { + return err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_only-err"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + return errors.New("failed to read byte from memory") + } + var err5 error + switch value2 { + case 0:case 1: + value3, ok3 := i.module.Memory().ReadByte(uint32(results1 + 1)) + if !ok3 { + return errors.New("failed to read byte from memory") + } + var enum4 Refusal + switch value3 { + case 0: + enum4 = RefusalNoCredential + case 1: + enum4 = RefusalMalformed + case 2: + enum4 = RefusalUntypedPeer + case 3: + enum4 = RefusalForeignDomain + default: + return errors.New("invalid refusal discriminant") + } + err5 = &ResultError[Refusal]{Value: enum4} + default: + err5 = errors.New("invalid variant discriminant for expected") + } + return err5 +} + +func (i *ResultErrorsInstance) ViaCheck( + ctx context.Context, + n uint32, +) (uint32, error) { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("via-check").Call(ctx, uint64(result0)) + if err1 != nil { + var default1 uint32 + return default1, err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_via-check"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + var default2 uint32 + return default2, errors.New("failed to read byte from memory") + } + var value7 uint32 + var err7 error + switch value2 { + case 0: + value3, ok3 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok3 { + var default3 uint32 + return default3, errors.New("failed to read i32 from memory") + } + result4 := uint32(value3) + value7 = result4 + case 1: + value5, ok5 := i.module.Memory().ReadByte(uint32(results1 + 4)) + if !ok5 { + var default5 uint32 + return default5, errors.New("failed to read byte from memory") + } + var enum6 Refusal + switch value5 { + case 0: + enum6 = RefusalNoCredential + case 1: + enum6 = RefusalMalformed + case 2: + enum6 = RefusalUntypedPeer + case 3: + enum6 = RefusalForeignDomain + default: + var default0 uint32 + return default0, errors.New("invalid refusal discriminant") + } + err7 = &ResultError[Refusal]{Value: enum6} + default: + err7 = errors.New("invalid variant discriminant for expected") + } + return value7, err7 +} + +func (i *ResultErrorsInstance) ViaProbe( + ctx context.Context, + n uint32, +) error { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("via-probe").Call(ctx, uint64(result0)) + if err1 != nil { + return err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_via-probe"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + return errors.New("failed to read byte from memory") + } + var err9 error + switch value2 { + case 0:case 1: + value3, ok3 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok3 { + return errors.New("failed to read i32 from memory") + } + result4 := uint32(value3) + ptr5, ok5 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok5 { + return errors.New("failed to read pointer from memory") + } + len6, ok6 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok6 { + return errors.New("failed to read length from memory") + } + buf7, ok7 := i.module.Memory().Read(ptr5, len6) + if !ok7 { + return errors.New("failed to read bytes from memory") + } + str7 := string(buf7) + value8 := Problem{ + Code: result4, + Detail: str7, + } + err9 = &ResultError[Problem]{Value: value8} + default: + err9 = errors.New("invalid variant discriminant for expected") + } + return err9 +} + +func (i *ResultErrorsInstance) ViaExplain( + ctx context.Context, + n uint32, +) (string, error) { + arg0 := n + result0 := uint32(arg0) + raw1, err1 := i.module.ExportedFunction("via-explain").Call(ctx, uint64(result0)) + if err1 != nil { + var default1 string + return default1, err1 + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_via-explain"); postFn != nil { + if _, err := postFn.Call(ctx, raw1...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results1 := raw1[0] + value2, ok2 := i.module.Memory().ReadByte(uint32(results1 + 0)) + if !ok2 { + var default2 string + return default2, errors.New("failed to read byte from memory") + } + var value19 string + var err19 error + switch value2 { + case 0: + ptr3, ok3 := i.module.Memory().ReadUint32Le(uint32(results1 + 4)) + if !ok3 { + var default3 string + return default3, errors.New("failed to read pointer from memory") + } + len4, ok4 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok4 { + var default4 string + return default4, errors.New("failed to read length from memory") + } + buf5, ok5 := i.module.Memory().Read(ptr3, len4) + if !ok5 { + var default5 string + return default5, errors.New("failed to read bytes from memory") + } + str5 := string(buf5) + value19 = str5 + case 1: + value6, ok6 := i.module.Memory().ReadByte(uint32(results1 + 4)) + if !ok6 { + var default6 string + return default6, errors.New("failed to read byte from memory") + } + var value18 Fault + switch value6 { + case 0: + value7, ok7 := i.module.Memory().ReadByte(uint32(results1 + 8)) + if !ok7 { + var default7 string + return default7, errors.New("failed to read byte from memory") + } + var enum8 Refusal + switch value7 { + case 0: + enum8 = RefusalNoCredential + case 1: + enum8 = RefusalMalformed + case 2: + enum8 = RefusalUntypedPeer + case 3: + enum8 = RefusalForeignDomain + default: + var default0 string + return default0, errors.New("invalid refusal discriminant") + } + value18 = FaultRefused{Value: enum8} + case 1: + value9, ok9 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok9 { + var default9 string + return default9, errors.New("failed to read i32 from memory") + } + result10 := uint32(value9) + ptr11, ok11 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok11 { + var default11 string + return default11, errors.New("failed to read pointer from memory") + } + len12, ok12 := i.module.Memory().ReadUint32Le(uint32(results1 + 16)) + if !ok12 { + var default12 string + return default12, errors.New("failed to read length from memory") + } + buf13, ok13 := i.module.Memory().Read(ptr11, len12) + if !ok13 { + var default13 string + return default13, errors.New("failed to read bytes from memory") + } + str13 := string(buf13) + value14 := Problem{ + Code: result10, + Detail: str13, + } + value18 = value14 + case 2: + ptr15, ok15 := i.module.Memory().ReadUint32Le(uint32(results1 + 8)) + if !ok15 { + var default15 string + return default15, errors.New("failed to read pointer from memory") + } + len16, ok16 := i.module.Memory().ReadUint32Le(uint32(results1 + 12)) + if !ok16 { + var default16 string + return default16, errors.New("failed to read length from memory") + } + buf17, ok17 := i.module.Memory().Read(ptr15, len16) + if !ok17 { + var default17 string + return default17, errors.New("failed to read bytes from memory") + } + str17 := string(buf17) + value18 = FaultMessage{Value: str17} + case 3: + value18 = FaultUnknown{} + default: + var default0 string + return default0, errors.New("invalid fault discriminant") + } + err19 = &ResultError[Fault]{Value: value18} + default: + err19 = errors.New("invalid variant discriminant for expected") + } + return value19, err19 +} + diff --git a/cmd/gravity/tests/cmd/result-errors.toml b/cmd/gravity/tests/cmd/result-errors.toml new file mode 100644 index 0000000..39712a2 --- /dev/null +++ b/cmd/gravity/tests/cmd/result-errors.toml @@ -0,0 +1,2 @@ +bin.name = "gravity" +args = "--world result-errors ../../target/wasm32-unknown-unknown/release/example_result_errors.wasm" diff --git a/examples/enum-collisions/Cargo.toml b/examples/enum-collisions/Cargo.toml new file mode 100644 index 0000000..99a8f3f --- /dev/null +++ b/examples/enum-collisions/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-enum-collisions" +version = "0.0.2" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "=0.62.0" +wit-component = "=0.259.0" diff --git a/examples/enum-collisions/enum_collisions_test.go b/examples/enum-collisions/enum_collisions_test.go new file mode 100644 index 0000000..f6c27ff --- /dev/null +++ b/examples/enum-collisions/enum_collisions_test.go @@ -0,0 +1,51 @@ +package enum_collisions + +import ( + "context" + "testing" +) + +// Every enum case is a constant named for its enum, so cases that share a +// name across enums, or with a type, are distinct identifiers. Before the +// fix the bindings failed go vet: PeerSvid and Point were redeclared. + +func newInstance(t *testing.T) *EnumCollisionsInstance { + t.Helper() + // The world's `use types.{...}` makes `types` an import with no + // functions, so its host argument has nothing to implement. + fac, err := NewEnumCollisionsFactory(t.Context(), nil) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { fac.Close(context.Background()) }) + ins, err := fac.Instantiate(t.Context()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { ins.Close(context.Background()) }) + return ins +} + +func TestSharedCaseNamesStayDistinct(t *testing.T) { + ins := newInstance(t) + for in, want := range map[CredentialKind]Via{ + CredentialKindPeerSvid: ViaPeerSvid, + CredentialKindSignedToken: ViaSignedToken, + CredentialKindBearer: ViaAnonymous, + } { + if got := ins.ViaFor(t.Context(), in); got != want { + t.Errorf("ViaFor(%v) = %v, want %v", in, got, want) + } + } +} + +func TestCaseNamedLikeAType(t *testing.T) { + ins := newInstance(t) + p := Point{X: 3, Y: -4} + if got := ins.Describe(t.Context(), ShapePoint, p); got != "point(3, -4)" { + t.Errorf("Describe(ShapePoint) = %q", got) + } + if got := ins.Describe(t.Context(), ShapeLine, p); got != "line to (3, -4)" { + t.Errorf("Describe(ShapeLine) = %q", got) + } +} diff --git a/examples/enum-collisions/src/lib.rs b/examples/enum-collisions/src/lib.rs new file mode 100644 index 0000000..cf80349 --- /dev/null +++ b/examples/enum-collisions/src/lib.rs @@ -0,0 +1,24 @@ +wit_bindgen::generate!({ + world: "enum-collisions", +}); + +struct EnumCollisions; + +export!(EnumCollisions); + +impl Guest for EnumCollisions { + fn via_for(k: CredentialKind) -> Via { + match k { + CredentialKind::PeerSvid => Via::PeerSvid, + CredentialKind::SignedToken => Via::SignedToken, + CredentialKind::Bearer => Via::Anonymous, + } + } + + fn describe(s: Shape, p: Point) -> String { + match s { + Shape::Point => format!("point({}, {})", p.x, p.y), + Shape::Line => format!("line to ({}, {})", p.x, p.y), + } + } +} diff --git a/examples/enum-collisions/wit/enum-collisions.wit b/examples/enum-collisions/wit/enum-collisions.wit new file mode 100644 index 0000000..fc9824a --- /dev/null +++ b/examples/enum-collisions/wit/enum-collisions.wit @@ -0,0 +1,22 @@ +package gravity:enum-collisions; + +// WIT scopes a case name to its enum. Gravity used to emit every case as an +// unqualified package constant, so these failed go vet: +// +// - `via` and `credential-kind` share `peer-svid` and `signed-token` +// (PeerSvid redeclared); +// - `shape`'s case `point` is also a record's name (Point redeclared). +interface types { + enum via { peer-svid, signed-token, anonymous } + enum credential-kind { peer-svid, signed-token, bearer } + + record point { x: s32, y: s32 } + enum shape { point, line } +} + +world enum-collisions { + use types.{via, credential-kind, point, shape}; + + export via-for: func(k: credential-kind) -> via; + export describe: func(s: shape, p: point) -> string; +} diff --git a/examples/generate.go b/examples/generate.go index fb5761c..fa202bd 100644 --- a/examples/generate.go +++ b/examples/generate.go @@ -7,6 +7,8 @@ package examples //go:generate cargo build -p example-instructions --target wasm32-unknown-unknown --release //go:generate cargo build -p example-regressions --target wasm32-unknown-unknown --release //go:generate cargo build -p example-variants --target wasm32-unknown-unknown --release +//go:generate cargo build -p example-enum-collisions --target wasm32-unknown-unknown --release +//go:generate cargo build -p example-result-errors --target wasm32-unknown-unknown --release //go:generate cargo run --bin gravity -- --world basic --output ./basic/basic.go ../target/wasm32-unknown-unknown/release/example_basic.wasm //go:generate cargo run --bin gravity -- --world records --output ./records/records.go ../target/wasm32-unknown-unknown/release/example_records.wasm @@ -15,3 +17,5 @@ package examples //go:generate cargo run --bin gravity -- --world instructions --output ./instructions/bindings.go ../target/wasm32-unknown-unknown/release/example_instructions.wasm //go:generate cargo run --bin gravity -- --world regressions --output ./regressions/regressions.go ../target/wasm32-unknown-unknown/release/example_regressions.wasm //go:generate cargo run --bin gravity -- --world variants --output ./variants/variants.go ../target/wasm32-unknown-unknown/release/example_variants.wasm +//go:generate cargo run --bin gravity -- --world enum-collisions --output ./enum-collisions/enum_collisions.go ../target/wasm32-unknown-unknown/release/example_enum_collisions.wasm +//go:generate cargo run --bin gravity -- --world result-errors --output ./result-errors/result_errors.go ../target/wasm32-unknown-unknown/release/example_result_errors.wasm diff --git a/examples/instructions/instructions_test.go b/examples/instructions/instructions_test.go index f29d046..8699c8c 100644 --- a/examples/instructions/instructions_test.go +++ b/examples/instructions/instructions_test.go @@ -236,9 +236,9 @@ func Test_EnumInput(t *testing.T) { } defer ins.Close(t.Context()) - ins.EnumInput(t.Context(), One) - ins.EnumInput(t.Context(), Two) - ins.EnumInput(t.Context(), Three) + ins.EnumInput(t.Context(), EnumValuesOne) + ins.EnumInput(t.Context(), EnumValuesTwo) + ins.EnumInput(t.Context(), EnumValuesThree) } // Test_IndirectParams covers the indirect parameter path of the canonical ABI. diff --git a/examples/memory/memory_test.go b/examples/memory/memory_test.go index 1ff160f..40dcbb8 100644 --- a/examples/memory/memory_test.go +++ b/examples/memory/memory_test.go @@ -28,19 +28,19 @@ var cases = map[string]Everything{ "min": { AU8: 0, AS8: math.MinInt8, AU16: 0, AS16: math.MinInt16, AU32: 0, AS32: math.MinInt32, AU64: 0, AS64: math.MinInt64, - AF64: -math.MaxFloat64, ABool: false, AChar: 0, AColor: Red, + AF64: -math.MaxFloat64, ABool: false, AChar: 0, AColor: ColorRed, AOpt: nil, AStr: "", AF32: -math.MaxFloat32, }, "max": { AU8: math.MaxUint8, AS8: math.MaxInt8, AU16: math.MaxUint16, AS16: math.MaxInt16, AU32: math.MaxUint32, AS32: math.MaxInt32, AU64: math.MaxUint64, AS64: math.MaxInt64, - AF64: math.MaxFloat64, ABool: true, AChar: 0x10FFFF, AColor: Blue, + AF64: math.MaxFloat64, ABool: true, AChar: 0x10FFFF, AColor: ColorBlue, AOpt: ptr[uint8](math.MaxUint8), AStr: "hello, world", AF32: math.MaxFloat32, }, "mixed": { AU8: 200, AS8: -5, AU16: 65000, AS16: -30000, AU32: 4_000_000_000, AS32: -42, AU64: 1 << 40, AS64: -(1 << 40), - AF64: -2.25, ABool: true, AChar: 'λ', AColor: Green, + AF64: -2.25, ABool: true, AChar: 'λ', AColor: ColorGreen, AOpt: ptr[uint8](0), AStr: "λ😀", AF32: 1.5, }, } diff --git a/examples/regressions/regressions_test.go b/examples/regressions/regressions_test.go index b6b660e..e7cfc6b 100644 --- a/examples/regressions/regressions_test.go +++ b/examples/regressions/regressions_test.go @@ -24,11 +24,11 @@ func (Checker) IsEnabled(_ context.Context, key string) bool { func (Checker) GetStatus(_ context.Context, key string) Status { switch key { case "active": - return Active + return StatusActive case "inactive": - return Inactive + return StatusInactive default: - return Unknown + return StatusUnknown } } @@ -65,11 +65,11 @@ type EmailChecker struct{} func (EmailChecker) IsAllowed(_ context.Context, email string) EmailCheckerValidatorResponse { switch email { case "allow@example.com": - return Yes + return EmailCheckerValidatorResponseYes case "block@example.com": - return No + return EmailCheckerValidatorResponseNo default: - return Maybe + return EmailCheckerValidatorResponseMaybe } } @@ -78,11 +78,11 @@ type BotVerifier struct{} func (BotVerifier) Verify(_ context.Context, botID string) BotVerifierValidatorResponse { switch botID { case "verified-bot": - return Verified + return BotVerifierValidatorResponseVerified case "spoofed-bot": - return Spoofed + return BotVerifierValidatorResponseSpoofed default: - return Unverifiable + return BotVerifierValidatorResponseUnverifiable } } @@ -151,9 +151,9 @@ func TestCheckStatus(t *testing.T) { key string want uint32 }{ - {"active", 0}, // Active - {"inactive", 1}, // Inactive - {"unknown", 2}, // Unknown + {"active", 0}, // StatusActive + {"inactive", 1}, // StatusInactive + {"unknown", 2}, // StatusUnknown } for _, tt := range tests { diff --git a/examples/regressions/wit/regressions.wit b/examples/regressions/wit/regressions.wit index e40ddb0..424f4b0 100644 --- a/examples/regressions/wit/regressions.wit +++ b/examples/regressions/wit/regressions.wit @@ -26,10 +26,9 @@ interface pinger { // `validator-response`. Without interface-scoped name qualification, the // generated Go redeclares the same type twice. interface email-checker { - // Case names overlap intentionally with the bot-verifier enum below, but - // `maybe` is used in place of `unknown` so the case CONSTANTS don't - // collide with `checker.status.unknown` (constant qualification is out - // of scope for this regression). + // Case constants are named for their enum (EmailCheckerValidatorResponseYes), + // so they could not collide with `checker.status.unknown` even if this enum + // had an `unknown`; the enum-collisions example covers shared case names. enum validator-response { yes, no, diff --git a/examples/result-errors/Cargo.toml b/examples/result-errors/Cargo.toml new file mode 100644 index 0000000..8667fdd --- /dev/null +++ b/examples/result-errors/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-result-errors" +version = "0.0.2" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "=0.62.0" +wit-component = "=0.259.0" diff --git a/examples/result-errors/result_errors_test.go b/examples/result-errors/result_errors_test.go new file mode 100644 index 0000000..3d2a032 --- /dev/null +++ b/examples/result-errors/result_errors_test.go @@ -0,0 +1,179 @@ +package result_errors + +import ( + "context" + "errors" + "reflect" + "strings" + "testing" +) + +// A result whose E is not a string returns its err case as a +// *ResultError[E]; errors.As recovers the E. Before the fix gravity panicked +// generating these bindings: "TODO(#4): implement remaining result conversion". + +func refusalFor(n uint32) Refusal { + return []Refusal{RefusalNoCredential, RefusalMalformed, RefusalUntypedPeer, RefusalForeignDomain}[n%4] +} + +// Host answers each import with the same err cases the guest's exports use, +// and with a plain error (one with no WIT representation) for n == 99. +type Host struct{} + +var errPlain = errors.New("not a WIT error") + +func (Host) Check(_ context.Context, n uint32) (uint32, error) { + switch { + case n == 99: + return 0, errPlain + case n < 4: + return 0, &ResultError[Refusal]{Value: refusalFor(n)} + } + return n + 1, nil +} + +func (Host) Probe(_ context.Context, n uint32) error { + if n%2 == 1 { + return &ResultError[Problem]{Value: Problem{Code: n, Detail: "host"}} + } + return nil +} + +func (Host) Explain(_ context.Context, n uint32) (string, error) { + switch n { + case 0: + return "", &ResultError[Fault]{Value: FaultRefused{Value: RefusalUntypedPeer}} + case 1: + return "", &ResultError[Fault]{Value: Problem{Code: 7, Detail: "x"}} + case 2: + return "", &ResultError[Fault]{Value: FaultMessage{Value: "from host"}} + case 3: + return "", &ResultError[Fault]{Value: FaultUnknown{}} + } + return "host fine", nil +} + +func newInstance(t *testing.T) *ResultErrorsInstance { + t.Helper() + fac, err := NewResultErrorsFactory(t.Context(), nil, Host{}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { fac.Close(context.Background()) }) + ins, err := fac.Instantiate(t.Context()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { ins.Close(context.Background()) }) + return ins +} + +// errValue recovers the E of a *ResultError[E], failing the test otherwise. +func errValue[E any](t *testing.T, err error) E { + t.Helper() + var re *ResultError[E] + if !errors.As(err, &re) { + var zero E + t.Fatalf("error %v (%T) is not a *ResultError[%T]", err, err, zero) + } + return re.Value +} + +func TestEnumErr(t *testing.T) { + ins := newInstance(t) + for n := range uint32(4) { + if _, err := ins.Decide(t.Context(), n); errValue[Refusal](t, err) != refusalFor(n) { + t.Errorf("Decide(%d): err %v, want %v", n, err, refusalFor(n)) + } + } + got, err := ins.Decide(t.Context(), 7) + if err != nil || got != (Decided{Name: "n7", N: 7}) { + t.Errorf("Decide(7) = %+v, %v", got, err) + } + // The enum's String names the WIT case, so the error reads as WIT wrote it. + if _, err := ins.Decide(t.Context(), 2); err == nil || err.Error() != "untyped-peer" { + t.Errorf("Decide(2) error text = %v, want untyped-peer", err) + } +} + +func TestRecordErr(t *testing.T) { + ins := newInstance(t) + if got, err := ins.Inspect(t.Context(), 4); err != nil || got != 8 { + t.Errorf("Inspect(4) = %d, %v", got, err) + } + _, err := ins.Inspect(t.Context(), 5) + if p := errValue[Problem](t, err); p != (Problem{Code: 5, Detail: "odd"}) { + t.Errorf("Inspect(5) err = %+v", p) + } +} + +func TestVariantErr(t *testing.T) { + ins := newInstance(t) + for n, want := range map[uint32]Fault{ + 0: FaultRefused{Value: RefusalForeignDomain}, + 1: Problem{Code: 1, Detail: "p"}, + 2: FaultMessage{Value: "m"}, + 3: FaultUnknown{}, + } { + _, err := ins.Classify(t.Context(), n) + if got := errValue[Fault](t, err); !reflect.DeepEqual(got, want) { + t.Errorf("Classify(%d) err = %#v, want %#v", n, got, want) + } + } + if got, err := ins.Classify(t.Context(), 9); err != nil || got != "fine" { + t.Errorf("Classify(9) = %q, %v", got, err) + } +} + +func TestErrWithoutOk(t *testing.T) { + ins := newInstance(t) + if err := ins.OnlyErr(t.Context(), 0); err != nil { + t.Errorf("OnlyErr(0) = %v", err) + } + if got := errValue[Refusal](t, ins.OnlyErr(t.Context(), 3)); got != RefusalForeignDomain { + t.Errorf("OnlyErr(3) err = %v", got) + } +} + +func TestHostReturnsTypedErrs(t *testing.T) { + ins := newInstance(t) + for n := range uint32(4) { + if _, err := ins.ViaCheck(t.Context(), n); errValue[Refusal](t, err) != refusalFor(n) { + t.Errorf("ViaCheck(%d) err = %v", n, err) + } + } + if got, err := ins.ViaCheck(t.Context(), 10); err != nil || got != 11 { + t.Errorf("ViaCheck(10) = %d, %v", got, err) + } + if err := ins.ViaProbe(t.Context(), 2); err != nil { + t.Errorf("ViaProbe(2) = %v", err) + } + if p := errValue[Problem](t, ins.ViaProbe(t.Context(), 3)); p != (Problem{Code: 3, Detail: "host"}) { + t.Errorf("ViaProbe(3) err = %+v", p) + } + for n, want := range map[uint32]Fault{ + 0: FaultRefused{Value: RefusalUntypedPeer}, + 1: Problem{Code: 7, Detail: "x"}, + 2: FaultMessage{Value: "from host"}, + 3: FaultUnknown{}, + } { + _, err := ins.ViaExplain(t.Context(), n) + if got := errValue[Fault](t, err); !reflect.DeepEqual(got, want) { + t.Errorf("ViaExplain(%d) err = %#v, want %#v", n, got, want) + } + } +} + +// A host error with no WIT representation traps the guest: the export +// returns an error, and it is not a *ResultError. +func TestHostPlainErrorTraps(t *testing.T) { + ins := newInstance(t) + _, err := ins.ViaCheck(t.Context(), 99) + var re *ResultError[Refusal] + if err == nil || errors.As(err, &re) { + t.Fatalf("ViaCheck(99) err = %v, want a trap", err) + } + if !strings.Contains(err.Error(), errPlain.Error()) { + t.Errorf("trap %q does not carry the host's error", err) + } +} diff --git a/examples/result-errors/src/lib.rs b/examples/result-errors/src/lib.rs new file mode 100644 index 0000000..8515fa4 --- /dev/null +++ b/examples/result-errors/src/lib.rs @@ -0,0 +1,71 @@ +use gravity::result_errors::host; + +wit_bindgen::generate!({ + world: "result-errors", +}); + +struct ResultErrors; + +export!(ResultErrors); + +fn refusal(n: u32) -> Refusal { + match n % 4 { + 0 => Refusal::NoCredential, + 1 => Refusal::Malformed, + 2 => Refusal::UntypedPeer, + _ => Refusal::ForeignDomain, + } +} + +impl Guest for ResultErrors { + fn decide(n: u32) -> Result { + if n < 4 { + Err(refusal(n)) + } else { + Ok(Decided { + name: format!("n{n}"), + n, + }) + } + } + + fn inspect(n: u32) -> Result { + if n.is_multiple_of(2) { + Ok(n * 2) + } else { + Err(Problem { + code: n, + detail: "odd".into(), + }) + } + } + + fn classify(n: u32) -> Result { + match n { + 0 => Err(Fault::Refused(Refusal::ForeignDomain)), + 1 => Err(Fault::Problem(Problem { + code: 1, + detail: "p".into(), + })), + 2 => Err(Fault::Message("m".into())), + 3 => Err(Fault::Unknown), + _ => Ok("fine".into()), + } + } + + fn only_err(n: u32) -> Result<(), Refusal> { + if n == 0 { Ok(()) } else { Err(refusal(n)) } + } + + fn via_check(n: u32) -> Result { + host::check(n) + } + + fn via_probe(n: u32) -> Result<(), Problem> { + host::probe(n) + } + + fn via_explain(n: u32) -> Result { + host::explain(n) + } +} diff --git a/examples/result-errors/wit/result-errors.wit b/examples/result-errors/wit/result-errors.wit new file mode 100644 index 0000000..5ad95c4 --- /dev/null +++ b/examples/result-errors/wit/result-errors.wit @@ -0,0 +1,39 @@ +package gravity:result-errors; + +// result where E is not a string: an enum, a record, a variant, with +// and without an ok type, returned by the guest and by the host. Gravity used +// to lower only result and panicked generating anything else +// ("TODO(#4): implement remaining result conversion"). +interface types { + enum refusal { no-credential, malformed, untyped-peer, foreign-domain } + record problem { code: u32, detail: string } + variant fault { + refused(refusal), + problem(problem), + message(string), + unknown, + } + record decided { name: string, n: u32 } +} + +interface host { + use types.{refusal, problem, fault}; + + check: func(n: u32) -> result; + probe: func(n: u32) -> result<_, problem>; + explain: func(n: u32) -> result; +} + +world result-errors { + use types.{refusal, problem, fault, decided}; + import host; + + export decide: func(n: u32) -> result; + export inspect: func(n: u32) -> result; + export classify: func(n: u32) -> result; + export only-err: func(n: u32) -> result<_, refusal>; + + export via-check: func(n: u32) -> result; + export via-probe: func(n: u32) -> result<_, problem>; + export via-explain: func(n: u32) -> result; +}