Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion cmd/gravity/src/codegen/bindings.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -77,10 +77,20 @@ impl<'a> Bindings<'a> {
analyzed_imports: &AnalyzedImports,
import_chains: BTreeMap<String, Tokens<Go>>,
) {
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)
}
Expand Down
29 changes: 29 additions & 0 deletions cmd/gravity/src/codegen/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, E>` 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<decided, refusal>;
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}"
);
}
}
62 changes: 60 additions & 2 deletions cmd/gravity/src/codegen/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
Expand All @@ -18,6 +18,9 @@ pub struct FactoryConfig<'a> {
pub analyzed_imports: &'a AnalyzedImports,
pub import_chains: BTreeMap<String, Tokens<Go>>,
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
Expand Down Expand Up @@ -69,6 +72,26 @@ impl<'a> FactoryGenerator<'a> {
};
}

/// Generate `ResultError`, the error a function returns for the err case
/// of a `result<T, E>` whose E is not a string.
fn generate_result_error(&self, tokens: &mut Tokens<Go>) {
quote_in! { *tokens =>
$(comment(&[
"ResultError is the error a function returns for the err case of a WIT",
"result<T, E> 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<Go>) {
let AnalyzedImports {
Expand Down Expand Up @@ -168,12 +191,16 @@ impl<'a> FormatInto<Go> 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},
Expand All @@ -194,11 +221,42 @@ 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();
generator.generate_write_string(&mut tokens);

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}"
);
}
}
}
119 changes: 115 additions & 4 deletions cmd/gravity/src/codegen/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<Go> = 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:
Expand Down Expand Up @@ -1498,7 +1609,7 @@ impl Bindgen for Func<'_> {

let mut cases: Tokens<Go> = 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:
Expand Down
Loading
Loading