From 1e52054b3d8b0c53b73132fdc2f36b93bb9912d5 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Tue, 14 Oct 2025 21:58:49 +0100 Subject: [PATCH 1/8] feat: add a 'records' example, and some required instructions Prior to this commit there was ostensibly no support for 'record' WIT types, but there wasn't actually much required for them to work. This commit adds an example for use in the test harness and the required instructions to load/store integer record fields. Relates to #4. --- Cargo.lock | 308 +++++++++++++++++++--- cmd/gravity/src/codegen/exports.rs | 2 +- cmd/gravity/src/codegen/func.rs | 190 +++++++++++-- cmd/gravity/tests/cmd/instructions.stdout | 2 +- cmd/gravity/tests/cmd/records.stderr | 0 cmd/gravity/tests/cmd/records.stdout | 277 +++++++++++++++++++ cmd/gravity/tests/cmd/records.toml | 2 + examples/generate.go | 2 + examples/records/Cargo.toml | 11 + examples/records/records_test.go | 66 +++++ examples/records/src/lib.rs | 31 +++ examples/records/wit/records.wit | 18 ++ 12 files changed, 859 insertions(+), 50 deletions(-) create mode 100644 cmd/gravity/tests/cmd/records.stderr create mode 100644 cmd/gravity/tests/cmd/records.stdout create mode 100644 cmd/gravity/tests/cmd/records.toml create mode 100644 examples/records/Cargo.toml create mode 100644 examples/records/records_test.go create mode 100644 examples/records/src/lib.rs create mode 100644 examples/records/wit/records.wit diff --git a/Cargo.lock b/Cargo.lock index 81b26f2..cb3cb0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,9 +89,9 @@ dependencies = [ "clap", "genco", "trycmd", - "wit-bindgen", - "wit-bindgen-core", - "wit-component", + "wit-bindgen 0.53.1", + "wit-bindgen-core 0.53.1", + "wit-component 0.245.1", ] [[package]] @@ -185,40 +185,142 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" name = "example-basic" version = "0.0.2" dependencies = [ - "wit-bindgen", - "wit-component", + "wit-bindgen 0.53.1", + "wit-component 0.245.1", ] [[package]] name = "example-iface-method-returns-string" version = "0.0.2" dependencies = [ - "wit-bindgen", - "wit-component", + "wit-bindgen 0.53.1", + "wit-component 0.245.1", ] [[package]] name = "example-instructions" version = "0.0.2" dependencies = [ - "wit-bindgen", - "wit-component", + "wit-bindgen 0.53.1", + "wit-component 0.245.1", +] + +[[package]] +name = "example-records" +version = "0.0.2" +dependencies = [ + "wit-bindgen 0.46.0", + "wit-component 0.239.0", ] [[package]] name = "example-regressions" version = "0.0.2" dependencies = [ - "wit-bindgen", - "wit-component", + "wit-bindgen 0.53.1", + "wit-component 0.245.1", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "genco" version = "0.19.0" @@ -247,13 +349,22 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -291,7 +402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -354,6 +465,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "prettyplease" version = "0.2.29" @@ -483,6 +600,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.13.2" @@ -616,6 +739,16 @@ dependencies = [ "libc", ] +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser 0.239.0", +] + [[package]] name = "wasm-encoder" version = "0.245.1" @@ -623,7 +756,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.239.0", + "wasmparser 0.239.0", ] [[package]] @@ -634,8 +779,20 @@ checksum = "da55e60097e8b37b475a0fa35c3420dd71d9eb7bd66109978ab55faf56a57efb" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -645,7 +802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ "bitflags", - "hashbrown", + "hashbrown 0.16.1", "indexmap", "semver", ] @@ -812,6 +969,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro 0.46.0", +] + [[package]] name = "wit-bindgen" version = "0.53.1" @@ -819,7 +988,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e915216dde3e818093168df8380a64fba25df468d626c80dd5d6a184c87e7c7" dependencies = [ "bitflags", - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.53.1", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.239.0", ] [[package]] @@ -830,7 +1010,23 @@ checksum = "3deda4b7e9f522d994906f6e6e0fc67965ea8660306940a776b76732be8f3933" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.245.1", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata 0.239.0", + "wit-bindgen-core 0.46.0", + "wit-component 0.239.0", ] [[package]] @@ -844,9 +1040,24 @@ dependencies = [ "indexmap", "prettyplease", "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.245.1", + "wit-bindgen-core 0.53.1", + "wit-component 0.245.1", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core 0.46.0", + "wit-bindgen-rust 0.46.0", ] [[package]] @@ -860,8 +1071,27 @@ dependencies = [ "proc-macro2", "quote", "syn", - "wit-bindgen-core", - "wit-bindgen-rust", + "wit-bindgen-core 0.53.1", + "wit-bindgen-rust 0.53.1", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.239.0", + "wasm-metadata 0.239.0", + "wasmparser 0.239.0", + "wit-parser 0.239.0", ] [[package]] @@ -877,10 +1107,28 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.245.1", + "wasm-metadata 0.245.1", + "wasmparser 0.245.1", + "wit-parser 0.245.1", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.239.0", ] [[package]] @@ -890,7 +1138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" dependencies = [ "anyhow", - "hashbrown", + "hashbrown 0.16.1", "id-arena", "indexmap", "log", @@ -899,5 +1147,5 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.245.1", ] diff --git a/cmd/gravity/src/codegen/exports.rs b/cmd/gravity/src/codegen/exports.rs index e155ee9..a4e89ed 100644 --- a/cmd/gravity/src/codegen/exports.rs +++ b/cmd/gravity/src/codegen/exports.rs @@ -169,7 +169,7 @@ mod tests { assert!(generated.contains("if err1 != nil {")); assert!(generated.contains("panic(err1)")); assert!(generated.contains("results1 := raw1[0]")); - assert!(generated.contains("result2 := uint32(results1)")); + assert!(generated.contains("result2 := api.DecodeU32(uint64(results1))")); assert!(generated.contains("return result2")); } } diff --git a/cmd/gravity/src/codegen/func.rs b/cmd/gravity/src/codegen/func.rs index c7c2e01..6fab0fb 100644 --- a/cmd/gravity/src/codegen/func.rs +++ b/cmd/gravity/src/codegen/func.rs @@ -12,7 +12,7 @@ use crate::{ imports::{ ERRORS_NEW, REFLECT_VALUE_OF, WAZERO_API_DECODE_F32, WAZERO_API_DECODE_F64, WAZERO_API_DECODE_I32, WAZERO_API_DECODE_U32, WAZERO_API_ENCODE_F32, - WAZERO_API_ENCODE_F64, WAZERO_API_ENCODE_I32, + WAZERO_API_ENCODE_F64, WAZERO_API_ENCODE_I32, WAZERO_API_ENCODE_U32, }, GoIdentifier, GoResult, GoType, Operand, }, @@ -310,20 +310,13 @@ impl Bindgen for Func<'_> { } results.push(Operand::SingleValue(value)) } - // I32FromU32 and U32FromI32 are identity operations at the Wasm - // level (both are 32-bit integers). We use a simple uint32 cast - // rather than api.EncodeU32/api.DecodeU32 because those functions - // convert between uint32 and uint64, but operands here are always - // uint32 — whether from host function params (imports), memory - // reads (exports), or Go variables. The uint64 conversion for - // api.Function.Call() is handled separately by CallWasm. Instruction::I32FromU32 => { let tmp = self.tmp(); let result = &format!("result{tmp}"); let operand = &operands[0]; quote_in! { self.body => $['\r'] - $result := uint32($operand) + $result := $WAZERO_API_ENCODE_U32($operand) }; results.push(Operand::SingleValue(result.into())); } @@ -333,7 +326,7 @@ impl Bindgen for Func<'_> { let operand = &operands[0]; quote_in! { self.body => $['\r'] - $result := uint32($operand) + $result := $WAZERO_API_DECODE_U32(uint64($operand)) }; results.push(Operand::SingleValue(result.into())); } @@ -1095,16 +1088,168 @@ impl Bindgen for Func<'_> { Instruction::I32Load8S { .. } => todo!("implement instruction: {inst:?}"), Instruction::I32Load16U { .. } => todo!("implement instruction: {inst:?}"), Instruction::I32Load16S { .. } => todo!("implement instruction: {inst:?}"), - Instruction::I64Load { .. } => todo!("implement instruction: {inst:?}"), - Instruction::F32Load { .. } => todo!("implement instruction: {inst:?}"), - Instruction::F64Load { .. } => todo!("implement instruction: {inst:?}"), + Instruction::I64Load { offset } => { + // TODO(#58): Support additional ArchitectureSize + let offset = offset.size_wasm32(); + let tmp = self.tmp(); + let value = &format!("value{tmp}"); + let ok = &format!("ok{tmp}"); + let default = &format!("default{tmp}"); + let operand = &operands[0]; + quote_in! { self.body => + $['\r'] + $value, $ok := i.module.Memory().ReadUint64Le(uint32($operand + $offset)) + $(match &self.result { + GoResult::Anon(GoType::ValueOrError(typ)) => { + if !$ok { + var $default $(typ.as_ref()) + return $default, $ERRORS_NEW("failed to read i64 from memory") + } + } + GoResult::Anon(GoType::Error) => { + if !$ok { + return $ERRORS_NEW("failed to read i64 from memory") + } + } + GoResult::Anon(_) | GoResult::Empty => { + $(comment(&["The return type doesn't contain an error so we panic if one is encountered"])) + if !$ok { + panic($ERRORS_NEW("failed to read i64 from memory")) + } + } + }) + }; + results.push(Operand::SingleValue(value.into())); + } + Instruction::F32Load { offset } => { + // TODO(#58): Support additional ArchitectureSize + let offset = offset.size_wasm32(); + let tmp = self.tmp(); + let value = &format!("value{tmp}"); + let ok = &format!("ok{tmp}"); + let default = &format!("default{tmp}"); + let operand = &operands[0]; + quote_in! { self.body => + $['\r'] + $value, $ok := i.module.Memory().ReadUint64Le(uint32($operand + $offset)) + $(match &self.result { + GoResult::Anon(GoType::ValueOrError(typ)) => { + if !$ok { + var $default $(typ.as_ref()) + return $default, $ERRORS_NEW("failed to read f64 from memory") + } + } + GoResult::Anon(GoType::Error) => { + if !$ok { + return $ERRORS_NEW("failed to read f64 from memory") + } + } + GoResult::Anon(_) | GoResult::Empty => { + $(comment(&["The return type doesn't contain an error so we panic if one is encountered"])) + if !$ok { + panic($ERRORS_NEW("failed to read f64 from memory")) + } + } + }) + }; + results.push(Operand::SingleValue(value.into())); + } + Instruction::F64Load { offset } => { + // TODO(#58): Support additional ArchitectureSize + let offset = offset.size_wasm32(); + let tmp = self.tmp(); + let value = &format!("value{tmp}"); + let ok = &format!("ok{tmp}"); + let default = &format!("default{tmp}"); + let operand = &operands[0]; + quote_in! { self.body => + $['\r'] + $value, $ok := i.module.Memory().ReadUint64Le(uint32($operand + $offset)) + $(match &self.result { + GoResult::Anon(GoType::ValueOrError(typ)) => { + if !$ok { + var $default $(typ.as_ref()) + return $default, $ERRORS_NEW("failed to read f64 from memory") + } + } + GoResult::Anon(GoType::Error) => { + if !$ok { + return $ERRORS_NEW("failed to read f64 from memory") + } + } + GoResult::Anon(_) | GoResult::Empty => { + $(comment(&["The return type doesn't contain an error so we panic if one is encountered"])) + if !$ok { + panic($ERRORS_NEW("failed to read f64 from memory")) + } + } + }) + }; + results.push(Operand::SingleValue(value.into())); + } Instruction::I32Store16 { .. } => todo!("implement instruction: {inst:?}"), Instruction::I64Store { .. } => todo!("implement instruction: {inst:?}"), - Instruction::F32Store { .. } => todo!("implement instruction: {inst:?}"), - Instruction::F64Store { .. } => todo!("implement instruction: {inst:?}"), + Instruction::F32Store { offset } => { + // TODO(#58): Support additional ArchitectureSize + let offset = offset.size_wasm32(); + let tag = &operands[0]; + let ptr = &operands[1]; + match &self.direction { + Direction::Export => { + quote_in! { self.body => + $['\r'] + i.module.Memory().WriteUint64Le($ptr+$offset, $tag) + } + } + Direction::Import { .. } => { + quote_in! { self.body => + $['\r'] + mod.Memory().WriteUint64Le($ptr+$offset, $tag) + } + } + } + } + Instruction::F64Store { offset } => { + // TODO(#58): Support additional ArchitectureSize + let offset = offset.size_wasm32(); + let tag = &operands[0]; + let ptr = &operands[1]; + match &self.direction { + Direction::Export => { + quote_in! { self.body => + $['\r'] + i.module.Memory().WriteUint64Le($ptr+$offset, $tag) + } + } + Direction::Import { .. } => { + quote_in! { self.body => + $['\r'] + mod.Memory().WriteUint64Le($ptr+$offset, $tag) + } + } + } + } Instruction::I32FromChar => todo!("implement instruction: {inst:?}"), - Instruction::I64FromU64 => todo!("implement instruction: {inst:?}"), - Instruction::I64FromS64 => todo!("implement instruction: {inst:?}"), + Instruction::I64FromU64 => { + let tmp = self.tmp(); + let value = format!("value{tmp}"); + let operand = &operands[0]; + quote_in! { self.body => + $['\r'] + $(&value) := int64($operand) + } + results.push(Operand::SingleValue(value.into())); + } + Instruction::I64FromS64 => { + let tmp = self.tmp(); + let value = format!("value{tmp}"); + let operand = &operands[0]; + quote_in! { self.body => + $['\r'] + $(&value) := $operand + } + results.push(Operand::SingleValue(value.into())); + } Instruction::I32FromS32 => { let tmp = self.tmp(); let value = format!("value{tmp}"); @@ -1204,7 +1349,16 @@ impl Bindgen for Func<'_> { results.push(Operand::SingleValue(result.into())); } Instruction::S64FromI64 => todo!("implement instruction: {inst:?}"), - Instruction::U64FromI64 => todo!("implement instruction: {inst:?}"), + Instruction::U64FromI64 => { + let tmp = self.tmp(); + let value = format!("value{tmp}"); + let operand = &operands[0]; + quote_in! { self.body => + $['\r'] + $(&value) := uint64($operand) + } + results.push(Operand::SingleValue(value.into())); + } Instruction::CharFromI32 => todo!("implement instruction: {inst:?}"), Instruction::F32FromCoreF32 => { let tmp = self.tmp(); diff --git a/cmd/gravity/tests/cmd/instructions.stdout b/cmd/gravity/tests/cmd/instructions.stdout index 7793b2a..655aa08 100644 --- a/cmd/gravity/tests/cmd/instructions.stdout +++ b/cmd/gravity/tests/cmd/instructions.stdout @@ -194,7 +194,7 @@ func (i *InstructionsInstance) U32Roundtrip( } results1 := raw1[0] - result2 := uint32(results1) + result2 := api.DecodeU32(uint64(results1)) return result2 } diff --git a/cmd/gravity/tests/cmd/records.stderr b/cmd/gravity/tests/cmd/records.stderr new file mode 100644 index 0000000..e69de29 diff --git a/cmd/gravity/tests/cmd/records.stdout b/cmd/gravity/tests/cmd/records.stdout new file mode 100644 index 0000000..96d71ef --- /dev/null +++ b/cmd/gravity/tests/cmd/records.stdout @@ -0,0 +1,277 @@ +// Code generated by arcjet-gravity; DO NOT EDIT. + +package records + +import "context" +import "errors" +import "github.com/tetratelabs/wazero" +import "github.com/tetratelabs/wazero/api" + +import _ "embed" + +//go:embed records.wasm +var wasmFileRecords []byte + +type IRecordsTypes interface {} + +type Foo struct { + Float32 float32 + + Float64 float64 + + Uint32 uint32 + + Uint64 uint64 + + S string + + Vf32 []float32 + + Vf64 []float64 +} + +type RecordsFactory struct { + runtime wazero.Runtime + module wazero.CompiledModule +} + +func NewRecordsFactory( + ctx context.Context, + types IRecordsTypes, +) (*RecordsFactory, error) { + wazeroRuntime := wazero.NewRuntime(ctx) + + _, err0 := wazeroRuntime.NewHostModuleBuilder("arcjet:records/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, wasmFileRecords) + if err != nil { + return nil, err + } + return &RecordsFactory{ + runtime: wazeroRuntime, + module: module, + }, nil +} + +func (f *RecordsFactory) Instantiate(ctx context.Context) (*RecordsInstance, error) { + if module, err := f.runtime.InstantiateModule(ctx, f.module, wazero.NewModuleConfig()); err != nil { + return nil, err + } else { + return &RecordsInstance{module}, nil + } +} + +func (f *RecordsFactory) Close(ctx context.Context) { + f.runtime.Close(ctx) +} + +type RecordsInstance struct { + module api.Module +} + +func (i *RecordsInstance) 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 *RecordsInstance) ModifyFoo( + ctx context.Context, + f Foo, +) Foo { + arg0 := f + float320 := arg0.Float32 + float640 := arg0.Float64 + uint320 := arg0.Uint32 + uint640 := arg0.Uint64 + s0 := arg0.S + vf320 := arg0.Vf32 + vf640 := arg0.Vf64 + result1 := api.EncodeF32(float320) + result2 := api.EncodeF64(float640) + result3 := api.EncodeU32(uint320) + value4 := int64(uint640) + memory5 := i.module.Memory() + realloc5 := i.module.ExportedFunction("cabi_realloc") + ptr5, len5, err5 := writeString(ctx, s0, memory5, realloc5) + // The return type doesn't contain an error so we panic if one is encountered + if err5 != nil { + panic(err5) + } + vec7 := vf320 + len7 := uint64(len(vec7)) + result7, err7 := i.module.ExportedFunction("cabi_realloc").Call(ctx, 0, 0, 4, len7 * 4) + // The return type doesn't contain an error so we panic if one is encountered + if err7 != nil { + panic(err7) + } + ptr7 := result7[0] + for idx := uint64(0); idx < len7; idx++ { + e := vec7[idx] + base := uint32(ptr7 + uint64(idx) * uint64(4)) + result6 := api.EncodeF32(e) + i.module.Memory().WriteUint64Le(base+0, result6) + } + vec9 := vf640 + len9 := uint64(len(vec9)) + result9, err9 := i.module.ExportedFunction("cabi_realloc").Call(ctx, 0, 0, 8, len9 * 8) + // The return type doesn't contain an error so we panic if one is encountered + if err9 != nil { + panic(err9) + } + ptr9 := result9[0] + for idx := uint64(0); idx < len9; idx++ { + e := vec9[idx] + base := uint32(ptr9 + uint64(idx) * uint64(8)) + result8 := api.EncodeF64(e) + i.module.Memory().WriteUint64Le(base+0, result8) + } + raw10, err10 := i.module.ExportedFunction("modify-foo").Call(ctx, uint64(result1), uint64(result2), uint64(result3), uint64(value4), uint64(ptr5), uint64(len5), uint64(ptr7), uint64(len7), uint64(ptr9), uint64(len9)) + // The return type doesn't contain an error so we panic if one is encountered + if err10 != nil { + panic(err10) + } + + // 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 _, err := i.module.ExportedFunction("cabi_post_modify-foo").Call(ctx, raw10...); 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")) + } + }() + + results10 := raw10[0] + value11, ok11 := i.module.Memory().ReadUint64Le(uint32(results10 + 0)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok11 { + panic(errors.New("failed to read f64 from memory")) + } + result12 := api.DecodeF32(value11) + value13, ok13 := i.module.Memory().ReadUint64Le(uint32(results10 + 8)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok13 { + panic(errors.New("failed to read f64 from memory")) + } + result14 := api.DecodeF64(value13) + value15, ok15 := i.module.Memory().ReadUint32Le(uint32(results10 + 16)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok15 { + panic(errors.New("failed to read i32 from memory")) + } + result16 := api.DecodeU32(uint64(value15)) + value17, ok17 := i.module.Memory().ReadUint64Le(uint32(results10 + 24)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok17 { + panic(errors.New("failed to read i64 from memory")) + } + value18 := uint64(value17) + ptr19, ok19 := i.module.Memory().ReadUint32Le(uint32(results10 + 32)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok19 { + panic(errors.New("failed to read pointer from memory")) + } + len20, ok20 := i.module.Memory().ReadUint32Le(uint32(results10 + 36)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok20 { + panic(errors.New("failed to read length from memory")) + } + buf21, ok21 := i.module.Memory().Read(ptr19, len20) + // The return type doesn't contain an error so we panic if one is encountered + if !ok21 { + panic(errors.New("failed to read bytes from memory")) + } + str21 := string(buf21) + ptr22, ok22 := i.module.Memory().ReadUint32Le(uint32(results10 + 40)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok22 { + panic(errors.New("failed to read pointer from memory")) + } + len23, ok23 := i.module.Memory().ReadUint32Le(uint32(results10 + 44)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok23 { + panic(errors.New("failed to read length from memory")) + } + base26 := ptr22 + len26 := len23 + result26 := make([]float32, len26) + for idx26 := uint32(0); idx26 < len26; idx26++ { + base := base26 + idx26 * 4 + value24, ok24 := i.module.Memory().ReadUint64Le(uint32(base + 0)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok24 { + panic(errors.New("failed to read f64 from memory")) + } + result25 := api.DecodeF32(value24) + result26[idx26] = result25 + } + ptr27, ok27 := i.module.Memory().ReadUint32Le(uint32(results10 + 48)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok27 { + panic(errors.New("failed to read pointer from memory")) + } + len28, ok28 := i.module.Memory().ReadUint32Le(uint32(results10 + 52)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok28 { + panic(errors.New("failed to read length from memory")) + } + base31 := ptr27 + len31 := len28 + result31 := make([]float64, len31) + for idx31 := uint32(0); idx31 < len31; idx31++ { + base := base31 + idx31 * 8 + value29, ok29 := i.module.Memory().ReadUint64Le(uint32(base + 0)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok29 { + panic(errors.New("failed to read f64 from memory")) + } + result30 := api.DecodeF64(value29) + result31[idx31] = result30 + } + value32 := Foo{ + Float32: result12, + Float64: result14, + Uint32: result16, + Uint64: value18, + S: str21, + Vf32: result26, + Vf64: result31, + } + return value32 +} + diff --git a/cmd/gravity/tests/cmd/records.toml b/cmd/gravity/tests/cmd/records.toml new file mode 100644 index 0000000..4cba21d --- /dev/null +++ b/cmd/gravity/tests/cmd/records.toml @@ -0,0 +1,2 @@ +bin.name = "gravity" +args = "--world records ../../target/wasm32-unknown-unknown/release/example_records.wasm" diff --git a/examples/generate.go b/examples/generate.go index 6f539b0..302d0f3 100644 --- a/examples/generate.go +++ b/examples/generate.go @@ -1,11 +1,13 @@ package examples //go:generate cargo build -p example-basic --target wasm32-unknown-unknown --release +//go:generate cargo build -p example-records --target wasm32-unknown-unknown --release //go:generate cargo build -p example-iface-method-returns-string --target wasm32-unknown-unknown --release //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 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 //go:generate cargo run --bin gravity -- --world example --output ./iface-method-returns-string/example.go ../target/wasm32-unknown-unknown/release/example_iface_method_returns_string.wasm //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 diff --git a/examples/records/Cargo.toml b/examples/records/Cargo.toml new file mode 100644 index 0000000..1ae90a3 --- /dev/null +++ b/examples/records/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-records" +version = "0.0.2" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "=0.46.0" +wit-component = "=0.239.0" diff --git a/examples/records/records_test.go b/examples/records/records_test.go new file mode 100644 index 0000000..83e6416 --- /dev/null +++ b/examples/records/records_test.go @@ -0,0 +1,66 @@ +package records + +import ( + "math" + "testing" +) + +type types struct{} + +func TestRecord(t *testing.T) { + tys := types{} + fac, err := NewRecordsFactory(t.Context(), tys) + if err != nil { + t.Fatal(err) + } + defer fac.Close(t.Context()) + + ins, err := fac.Instantiate(t.Context()) + if err != nil { + t.Fatal(err) + } + defer ins.Close(t.Context()) + + foo := Foo{ + Float32: 1.0, + Float64: 1.0, + Uint32: 1, + Uint64: uint64(math.MaxUint32), + S: "hello", + Vf32: []float32{1.0, 2.0, 3.0}, + Vf64: []float64{1.0, 2.0, 3.0}, + } + got := ins.ModifyFoo(t.Context(), foo) + want := Foo{ + Float32: foo.Float32 * 2.0, + Float64: foo.Float64 * 2.0, + Uint32: foo.Uint32 + 1, + Uint64: foo.Uint64 + 1, + S: "received hello", + Vf32: []float32{2.0, 4.0, 6.0}, + Vf64: []float64{2.0, 4.0, 6.0}, + } + if !fooCmp(got, want) { + t.Fatalf("got %+v, want %+v", got, want) + } +} + +func fooCmp(a, b Foo) bool { + if a.Float32 != b.Float32 || a.Float64 != b.Float64 || a.Uint32 != b.Uint32 || a.Uint64 != b.Uint64 || a.S != b.S { + return false + } + if len(a.Vf32) != len(b.Vf32) || len(a.Vf64) != len(b.Vf64) { + return false + } + for i := range a.Vf32 { + if a.Vf32[i] != b.Vf32[i] { + return false + } + } + for i := range a.Vf64 { + if a.Vf64[i] != b.Vf64[i] { + return false + } + } + return true +} diff --git a/examples/records/src/lib.rs b/examples/records/src/lib.rs new file mode 100644 index 0000000..f70d0f5 --- /dev/null +++ b/examples/records/src/lib.rs @@ -0,0 +1,31 @@ +wit_bindgen::generate!({ + world: "records", +}); + +struct RecordsWorld; + +export!(RecordsWorld); + +impl Guest for RecordsWorld { + fn modify_foo( + Foo { + float64, + float32, + uint32, + uint64, + s, + vf32, + vf64, + }: Foo, + ) -> Foo { + Foo { + float64: float64 * 2.0, + float32: float32 * 2.0, + uint32: uint32 + 1, + uint64: uint64 + 1, + s: format!("received {s}"), + vf32: vf32.iter().map(|v| v * 2.0).collect(), + vf64: vf64.iter().map(|v| v * 2.0).collect(), + } + } +} diff --git a/examples/records/wit/records.wit b/examples/records/wit/records.wit new file mode 100644 index 0000000..b467229 --- /dev/null +++ b/examples/records/wit/records.wit @@ -0,0 +1,18 @@ +package arcjet:records; + +interface types { + record foo { + float32: f32, + float64: f64, + uint32: u32, + uint64: u64, + s: string, + vf32: list, + vf64: list, + } +} + +world records { + use types.{foo}; + export modify-foo: func(f: foo) -> foo; +} From 2bacc0ea587d27b3c88338db533bf60c0bcc2385 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 4 Dec 2025 21:24:33 +0000 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: blaine-arcjet <146491715+blaine-arcjet@users.noreply.github.com> --- cmd/gravity/src/codegen/func.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/gravity/src/codegen/func.rs b/cmd/gravity/src/codegen/func.rs index 6fab0fb..d820f9e 100644 --- a/cmd/gravity/src/codegen/func.rs +++ b/cmd/gravity/src/codegen/func.rs @@ -1136,18 +1136,18 @@ impl Bindgen for Func<'_> { GoResult::Anon(GoType::ValueOrError(typ)) => { if !$ok { var $default $(typ.as_ref()) - return $default, $ERRORS_NEW("failed to read f64 from memory") + return $default, $ERRORS_NEW("failed to read f32 from memory") } } GoResult::Anon(GoType::Error) => { if !$ok { - return $ERRORS_NEW("failed to read f64 from memory") + return $ERRORS_NEW("failed to read f32 from memory") } } GoResult::Anon(_) | GoResult::Empty => { $(comment(&["The return type doesn't contain an error so we panic if one is encountered"])) if !$ok { - panic($ERRORS_NEW("failed to read f64 from memory")) + panic($ERRORS_NEW("failed to read f32 from memory")) } } }) From 201df7abf4957f3a16d519a33c22f088ef329c18 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 4 Dec 2025 21:34:59 +0000 Subject: [PATCH 3/8] Add fallible records test --- examples/records/records_test.go | 74 ++++++++++++++++++++++++++++++++ examples/records/src/lib.rs | 26 +++++++++++ examples/records/wit/records.wit | 1 + 3 files changed, 101 insertions(+) diff --git a/examples/records/records_test.go b/examples/records/records_test.go index 83e6416..077e975 100644 --- a/examples/records/records_test.go +++ b/examples/records/records_test.go @@ -45,6 +45,80 @@ func TestRecord(t *testing.T) { } } +func TestRecordFallibleSuccess(t *testing.T) { + tys := types{} + fac, err := NewRecordsFactory(t.Context(), tys) + if err != nil { + t.Fatal(err) + } + defer fac.Close(t.Context()) + + ins, err := fac.Instantiate(t.Context()) + if err != nil { + t.Fatal(err) + } + defer ins.Close(t.Context()) + + foo := Foo{ + Float32: 1.0, + Float64: 5.0, // <= 10.0, should succeed + Uint32: 1, + Uint64: uint64(math.MaxUint32), + S: "hello", + Vf32: []float32{1.0, 2.0, 3.0}, + Vf64: []float64{1.0, 2.0, 3.0}, + } + got, err := ins.ModifyFooFallible(t.Context(), foo) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := Foo{ + Float32: foo.Float32 * 2.0, + Float64: foo.Float64 * 2.0, + Uint32: foo.Uint32 + 1, + Uint64: foo.Uint64 + 1, + S: "received hello", + Vf32: []float32{2.0, 4.0, 6.0}, + Vf64: []float64{2.0, 4.0, 6.0}, + } + if !fooCmp(got, want) { + t.Fatalf("got %+v, want %+v", got, want) + } +} + +func TestRecordFallibleError(t *testing.T) { + tys := types{} + fac, err := NewRecordsFactory(t.Context(), tys) + if err != nil { + t.Fatal(err) + } + defer fac.Close(t.Context()) + + ins, err := fac.Instantiate(t.Context()) + if err != nil { + t.Fatal(err) + } + defer ins.Close(t.Context()) + + foo := Foo{ + Float32: 1.0, + Float64: 15.0, // > 10.0, should error + Uint32: 1, + Uint64: uint64(math.MaxUint32), + S: "hello", + Vf32: []float32{1.0, 2.0, 3.0}, + Vf64: []float64{1.0, 2.0, 3.0}, + } + _, err = ins.ModifyFooFallible(t.Context(), foo) + if err == nil { + t.Fatal("expected error, got nil") + } + wantErr := "float64 too big" + if err.Error() != wantErr { + t.Fatalf("got error %q, want %q", err.Error(), wantErr) + } +} + func fooCmp(a, b Foo) bool { if a.Float32 != b.Float32 || a.Float64 != b.Float64 || a.Uint32 != b.Uint32 || a.Uint64 != b.Uint64 || a.S != b.S { return false diff --git a/examples/records/src/lib.rs b/examples/records/src/lib.rs index f70d0f5..4315259 100644 --- a/examples/records/src/lib.rs +++ b/examples/records/src/lib.rs @@ -28,4 +28,30 @@ impl Guest for RecordsWorld { vf64: vf64.iter().map(|v| v * 2.0).collect(), } } + + fn modify_foo_fallible( + Foo { + float64, + float32, + uint32, + uint64, + s, + vf32, + vf64, + }: Foo, + ) -> Result { + if float64 > 10.0 { + Err("float64 too big".to_string()) + } else { + Ok(Foo { + float64: float64 * 2.0, + float32: float32 * 2.0, + uint32: uint32 + 1, + uint64: uint64 + 1, + s: format!("received {s}"), + vf32: vf32.iter().map(|v| v * 2.0).collect(), + vf64: vf64.iter().map(|v| v * 2.0).collect(), + }) + } + } } diff --git a/examples/records/wit/records.wit b/examples/records/wit/records.wit index b467229..c3cfb14 100644 --- a/examples/records/wit/records.wit +++ b/examples/records/wit/records.wit @@ -15,4 +15,5 @@ interface types { world records { use types.{foo}; export modify-foo: func(f: foo) -> foo; + export modify-foo-fallible: func(f: foo) -> result; } From 5836abf6f0d6032661ae7bbc4299417e40960b1d Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 4 Dec 2025 21:37:45 +0000 Subject: [PATCH 4/8] Define record type inline, not in separate interface --- examples/records/records_test.go | 11 +++-------- examples/records/wit/records.wit | 5 +---- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/records/records_test.go b/examples/records/records_test.go index 077e975..f1e0201 100644 --- a/examples/records/records_test.go +++ b/examples/records/records_test.go @@ -5,11 +5,8 @@ import ( "testing" ) -type types struct{} - func TestRecord(t *testing.T) { - tys := types{} - fac, err := NewRecordsFactory(t.Context(), tys) + fac, err := NewRecordsFactory(t.Context()) if err != nil { t.Fatal(err) } @@ -46,8 +43,7 @@ func TestRecord(t *testing.T) { } func TestRecordFallibleSuccess(t *testing.T) { - tys := types{} - fac, err := NewRecordsFactory(t.Context(), tys) + fac, err := NewRecordsFactory(t.Context()) if err != nil { t.Fatal(err) } @@ -87,8 +83,7 @@ func TestRecordFallibleSuccess(t *testing.T) { } func TestRecordFallibleError(t *testing.T) { - tys := types{} - fac, err := NewRecordsFactory(t.Context(), tys) + fac, err := NewRecordsFactory(t.Context()) if err != nil { t.Fatal(err) } diff --git a/examples/records/wit/records.wit b/examples/records/wit/records.wit index c3cfb14..015bfd6 100644 --- a/examples/records/wit/records.wit +++ b/examples/records/wit/records.wit @@ -1,6 +1,6 @@ package arcjet:records; -interface types { +world records { record foo { float32: f32, float64: f64, @@ -10,10 +10,7 @@ interface types { vf32: list, vf64: list, } -} -world records { - use types.{foo}; export modify-foo: func(f: foo) -> foo; export modify-foo-fallible: func(f: foo) -> result; } From 2850ed69228d544f4b1bdfe04a0d5f9619b26fa7 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Thu, 4 Dec 2025 21:39:34 +0000 Subject: [PATCH 5/8] Update CLI tests --- cmd/gravity/tests/cmd/records.stdout | 212 +++++++++++++++++++++++++-- 1 file changed, 201 insertions(+), 11 deletions(-) diff --git a/cmd/gravity/tests/cmd/records.stdout b/cmd/gravity/tests/cmd/records.stdout index 96d71ef..4745179 100644 --- a/cmd/gravity/tests/cmd/records.stdout +++ b/cmd/gravity/tests/cmd/records.stdout @@ -12,8 +12,6 @@ import _ "embed" //go:embed records.wasm var wasmFileRecords []byte -type IRecordsTypes interface {} - type Foo struct { Float32 float32 @@ -37,16 +35,9 @@ type RecordsFactory struct { func NewRecordsFactory( ctx context.Context, - types IRecordsTypes, ) (*RecordsFactory, error) { wazeroRuntime := wazero.NewRuntime(ctx) - _, err0 := wazeroRuntime.NewHostModuleBuilder("arcjet:records/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, wasmFileRecords) @@ -180,7 +171,7 @@ func (i *RecordsInstance) ModifyFoo( value11, ok11 := i.module.Memory().ReadUint64Le(uint32(results10 + 0)) // The return type doesn't contain an error so we panic if one is encountered if !ok11 { - panic(errors.New("failed to read f64 from memory")) + panic(errors.New("failed to read f32 from memory")) } result12 := api.DecodeF32(value11) value13, ok13 := i.module.Memory().ReadUint64Le(uint32(results10 + 8)) @@ -235,7 +226,7 @@ func (i *RecordsInstance) ModifyFoo( value24, ok24 := i.module.Memory().ReadUint64Le(uint32(base + 0)) // The return type doesn't contain an error so we panic if one is encountered if !ok24 { - panic(errors.New("failed to read f64 from memory")) + panic(errors.New("failed to read f32 from memory")) } result25 := api.DecodeF32(value24) result26[idx26] = result25 @@ -275,3 +266,202 @@ func (i *RecordsInstance) ModifyFoo( return value32 } +func (i *RecordsInstance) ModifyFooFallible( + ctx context.Context, + f Foo, +) (Foo, error) { + arg0 := f + float320 := arg0.Float32 + float640 := arg0.Float64 + uint320 := arg0.Uint32 + uint640 := arg0.Uint64 + s0 := arg0.S + vf320 := arg0.Vf32 + vf640 := arg0.Vf64 + result1 := api.EncodeF32(float320) + result2 := api.EncodeF64(float640) + result3 := api.EncodeU32(uint320) + value4 := int64(uint640) + memory5 := i.module.Memory() + realloc5 := i.module.ExportedFunction("cabi_realloc") + ptr5, len5, err5 := writeString(ctx, s0, memory5, realloc5) + if err5 != nil { + var default5 Foo + return default5, err5 + } + vec7 := vf320 + len7 := uint64(len(vec7)) + result7, err7 := i.module.ExportedFunction("cabi_realloc").Call(ctx, 0, 0, 4, len7 * 4) + if err7 != nil { + var default7 Foo + return default7, err7 + } + ptr7 := result7[0] + for idx := uint64(0); idx < len7; idx++ { + e := vec7[idx] + base := uint32(ptr7 + uint64(idx) * uint64(4)) + result6 := api.EncodeF32(e) + i.module.Memory().WriteUint64Le(base+0, result6) + } + vec9 := vf640 + len9 := uint64(len(vec9)) + result9, err9 := i.module.ExportedFunction("cabi_realloc").Call(ctx, 0, 0, 8, len9 * 8) + if err9 != nil { + var default9 Foo + return default9, err9 + } + ptr9 := result9[0] + for idx := uint64(0); idx < len9; idx++ { + e := vec9[idx] + base := uint32(ptr9 + uint64(idx) * uint64(8)) + result8 := api.EncodeF64(e) + i.module.Memory().WriteUint64Le(base+0, result8) + } + raw10, err10 := i.module.ExportedFunction("modify-foo-fallible").Call(ctx, uint64(result1), uint64(result2), uint64(result3), uint64(value4), uint64(ptr5), uint64(len5), uint64(ptr7), uint64(len7), uint64(ptr9), uint64(len9)) + if err10 != nil { + var default10 Foo + return default10, err10 + } + + // 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 _, err := i.module.ExportedFunction("cabi_post_modify-foo-fallible").Call(ctx, raw10...); 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")) + } + }() + + results10 := raw10[0] + value11, ok11 := i.module.Memory().ReadByte(uint32(results10 + 0)) + if !ok11 { + var default11 Foo + return default11, errors.New("failed to read byte from memory") + } + var value37 Foo + var err37 error + switch value11 { + case 0: + value12, ok12 := i.module.Memory().ReadUint64Le(uint32(results10 + 8)) + if !ok12 { + var default12 Foo + return default12, errors.New("failed to read f32 from memory") + } + result13 := api.DecodeF32(value12) + value14, ok14 := i.module.Memory().ReadUint64Le(uint32(results10 + 16)) + if !ok14 { + var default14 Foo + return default14, errors.New("failed to read f64 from memory") + } + result15 := api.DecodeF64(value14) + value16, ok16 := i.module.Memory().ReadUint32Le(uint32(results10 + 24)) + if !ok16 { + var default16 Foo + return default16, errors.New("failed to read i32 from memory") + } + result17 := api.DecodeU32(uint64(value16)) + value18, ok18 := i.module.Memory().ReadUint64Le(uint32(results10 + 32)) + if !ok18 { + var default18 Foo + return default18, errors.New("failed to read i64 from memory") + } + value19 := uint64(value18) + ptr20, ok20 := i.module.Memory().ReadUint32Le(uint32(results10 + 40)) + if !ok20 { + var default20 Foo + return default20, errors.New("failed to read pointer from memory") + } + len21, ok21 := i.module.Memory().ReadUint32Le(uint32(results10 + 44)) + if !ok21 { + var default21 Foo + return default21, errors.New("failed to read length from memory") + } + buf22, ok22 := i.module.Memory().Read(ptr20, len21) + if !ok22 { + var default22 Foo + return default22, errors.New("failed to read bytes from memory") + } + str22 := string(buf22) + ptr23, ok23 := i.module.Memory().ReadUint32Le(uint32(results10 + 48)) + if !ok23 { + var default23 Foo + return default23, errors.New("failed to read pointer from memory") + } + len24, ok24 := i.module.Memory().ReadUint32Le(uint32(results10 + 52)) + if !ok24 { + var default24 Foo + return default24, errors.New("failed to read length from memory") + } + base27 := ptr23 + len27 := len24 + result27 := make([]float32, len27) + for idx27 := uint32(0); idx27 < len27; idx27++ { + base := base27 + idx27 * 4 + value25, ok25 := i.module.Memory().ReadUint64Le(uint32(base + 0)) + if !ok25 { + var default25 Foo + return default25, errors.New("failed to read f32 from memory") + } + result26 := api.DecodeF32(value25) + result27[idx27] = result26 + } + ptr28, ok28 := i.module.Memory().ReadUint32Le(uint32(results10 + 56)) + if !ok28 { + var default28 Foo + return default28, errors.New("failed to read pointer from memory") + } + len29, ok29 := i.module.Memory().ReadUint32Le(uint32(results10 + 60)) + if !ok29 { + var default29 Foo + return default29, errors.New("failed to read length from memory") + } + base32 := ptr28 + len32 := len29 + result32 := make([]float64, len32) + for idx32 := uint32(0); idx32 < len32; idx32++ { + base := base32 + idx32 * 8 + value30, ok30 := i.module.Memory().ReadUint64Le(uint32(base + 0)) + if !ok30 { + var default30 Foo + return default30, errors.New("failed to read f64 from memory") + } + result31 := api.DecodeF64(value30) + result32[idx32] = result31 + } + value33 := Foo{ + Float32: result13, + Float64: result15, + Uint32: result17, + Uint64: value19, + S: str22, + Vf32: result27, + Vf64: result32, + } + value37 = value33 + case 1: + ptr34, ok34 := i.module.Memory().ReadUint32Le(uint32(results10 + 8)) + if !ok34 { + var default34 Foo + return default34, errors.New("failed to read pointer from memory") + } + len35, ok35 := i.module.Memory().ReadUint32Le(uint32(results10 + 12)) + if !ok35 { + var default35 Foo + return default35, errors.New("failed to read length from memory") + } + buf36, ok36 := i.module.Memory().Read(ptr34, len35) + if !ok36 { + var default36 Foo + return default36, errors.New("failed to read bytes from memory") + } + str36 := string(buf36) + err37 = errors.New(str36) + default: + err37 = errors.New("invalid variant discriminant for expected") + } + return value37, err37 +} + From 421d0d8a570cbda9d5c873dc4e89aad42df3aec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B6Rei?= Date: Wed, 4 Mar 2026 13:38:55 -0800 Subject: [PATCH 6/8] fix: make I32FromU32/U32FromI32 direction-aware In the import direction, function params are already uint32, so using api.EncodeU32/api.DecodeU32 is a wasteful roundtrip. Use simple uint32() casts for imports and the API functions for exports where CallWasm operates on uint64 values. Co-Authored-By: Claude Opus 4.6 --- cmd/gravity/src/codegen/func.rs | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/cmd/gravity/src/codegen/func.rs b/cmd/gravity/src/codegen/func.rs index d820f9e..71e4edf 100644 --- a/cmd/gravity/src/codegen/func.rs +++ b/cmd/gravity/src/codegen/func.rs @@ -314,20 +314,40 @@ impl Bindgen for Func<'_> { let tmp = self.tmp(); let result = &format!("result{tmp}"); let operand = &operands[0]; - quote_in! { self.body => - $['\r'] - $result := $WAZERO_API_ENCODE_U32($operand) - }; + match &self.direction { + Direction::Export => { + quote_in! { self.body => + $['\r'] + $result := $WAZERO_API_ENCODE_U32($operand) + }; + } + Direction::Import { .. } => { + quote_in! { self.body => + $['\r'] + $result := uint32($operand) + }; + } + } results.push(Operand::SingleValue(result.into())); } Instruction::U32FromI32 => { let tmp = self.tmp(); let result = &format!("result{tmp}"); let operand = &operands[0]; - quote_in! { self.body => - $['\r'] - $result := $WAZERO_API_DECODE_U32(uint64($operand)) - }; + match &self.direction { + Direction::Export => { + quote_in! { self.body => + $['\r'] + $result := $WAZERO_API_DECODE_U32(uint64($operand)) + }; + } + Direction::Import { .. } => { + quote_in! { self.body => + $['\r'] + $result := uint32($operand) + }; + } + } results.push(Operand::SingleValue(result.into())); } Instruction::PointerLoad { offset } => { From 69705519d26a3edaf4a6636dbd0efb1545f57898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B6Rei?= Date: Wed, 4 Mar 2026 13:41:03 -0800 Subject: [PATCH 7/8] chore: update deps and regenerate snapshots - Update examples/records wit-bindgen to 0.53.1 and wit-component to 0.245.1 to match other examples - Regenerate CLI test snapshots to reflect the EncodeU32/DecodeU32 codegen changes for export functions Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 304 +++------------------- cmd/gravity/tests/cmd/instructions.stdout | 2 +- cmd/gravity/tests/cmd/records.stdout | 30 +-- cmd/gravity/tests/cmd/regressions.stdout | 6 +- examples/records/Cargo.toml | 4 +- 5 files changed, 52 insertions(+), 294 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb3cb0e..fa49102 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,9 +89,9 @@ dependencies = [ "clap", "genco", "trycmd", - "wit-bindgen 0.53.1", - "wit-bindgen-core 0.53.1", - "wit-component 0.245.1", + "wit-bindgen", + "wit-bindgen-core", + "wit-component", ] [[package]] @@ -185,142 +185,48 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" name = "example-basic" version = "0.0.2" dependencies = [ - "wit-bindgen 0.53.1", - "wit-component 0.245.1", + "wit-bindgen", + "wit-component", ] [[package]] name = "example-iface-method-returns-string" version = "0.0.2" dependencies = [ - "wit-bindgen 0.53.1", - "wit-component 0.245.1", + "wit-bindgen", + "wit-component", ] [[package]] name = "example-instructions" version = "0.0.2" dependencies = [ - "wit-bindgen 0.53.1", - "wit-component 0.245.1", + "wit-bindgen", + "wit-component", ] [[package]] name = "example-records" version = "0.0.2" dependencies = [ - "wit-bindgen 0.46.0", - "wit-component 0.239.0", + "wit-bindgen", + "wit-component", ] [[package]] name = "example-regressions" version = "0.0.2" dependencies = [ - "wit-bindgen 0.53.1", - "wit-component 0.245.1", + "wit-bindgen", + "wit-component", ] -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - [[package]] name = "genco" version = "0.19.0" @@ -349,22 +255,13 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -402,7 +299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown", "serde", "serde_core", ] @@ -465,12 +362,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - [[package]] name = "prettyplease" version = "0.2.29" @@ -600,12 +491,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - [[package]] name = "smallvec" version = "1.13.2" @@ -739,16 +624,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wasm-encoder" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" -dependencies = [ - "leb128fmt", - "wasmparser 0.239.0", -] - [[package]] name = "wasm-encoder" version = "0.245.1" @@ -756,19 +631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" dependencies = [ "leb128fmt", - "wasmparser 0.245.1", -] - -[[package]] -name = "wasm-metadata" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder 0.239.0", - "wasmparser 0.239.0", + "wasmparser", ] [[package]] @@ -779,20 +642,8 @@ checksum = "da55e60097e8b37b475a0fa35c3420dd71d9eb7bd66109978ab55faf56a57efb" dependencies = [ "anyhow", "indexmap", - "wasm-encoder 0.245.1", - "wasmparser 0.245.1", -] - -[[package]] -name = "wasmparser" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "wasm-encoder", + "wasmparser", ] [[package]] @@ -802,7 +653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ "bitflags", - "hashbrown 0.16.1", + "hashbrown", "indexmap", "semver", ] @@ -969,18 +820,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -dependencies = [ - "bitflags", - "futures", - "once_cell", - "wit-bindgen-rust-macro 0.46.0", -] - [[package]] name = "wit-bindgen" version = "0.53.1" @@ -988,18 +827,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e915216dde3e818093168df8380a64fba25df468d626c80dd5d6a184c87e7c7" dependencies = [ "bitflags", - "wit-bindgen-rust-macro 0.53.1", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" -dependencies = [ - "anyhow", - "heck", - "wit-parser 0.239.0", + "wit-bindgen-rust-macro", ] [[package]] @@ -1010,23 +838,7 @@ checksum = "3deda4b7e9f522d994906f6e6e0fc67965ea8660306940a776b76732be8f3933" dependencies = [ "anyhow", "heck", - "wit-parser 0.245.1", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata 0.239.0", - "wit-bindgen-core 0.46.0", - "wit-component 0.239.0", + "wit-parser", ] [[package]] @@ -1040,24 +852,9 @@ dependencies = [ "indexmap", "prettyplease", "syn", - "wasm-metadata 0.245.1", - "wit-bindgen-core 0.53.1", - "wit-component 0.245.1", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core 0.46.0", - "wit-bindgen-rust 0.46.0", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", ] [[package]] @@ -1071,27 +868,8 @@ dependencies = [ "proc-macro2", "quote", "syn", - "wit-bindgen-core 0.53.1", - "wit-bindgen-rust 0.53.1", -] - -[[package]] -name = "wit-component" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.239.0", - "wasm-metadata 0.239.0", - "wasmparser 0.239.0", - "wit-parser 0.239.0", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] @@ -1107,28 +885,10 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.245.1", - "wasm-metadata 0.245.1", - "wasmparser 0.245.1", - "wit-parser 0.245.1", -] - -[[package]] -name = "wit-parser" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.239.0", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] @@ -1138,7 +898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" dependencies = [ "anyhow", - "hashbrown 0.16.1", + "hashbrown", "id-arena", "indexmap", "log", @@ -1147,5 +907,5 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.245.1", + "wasmparser", ] diff --git a/cmd/gravity/tests/cmd/instructions.stdout b/cmd/gravity/tests/cmd/instructions.stdout index 655aa08..4481ba8 100644 --- a/cmd/gravity/tests/cmd/instructions.stdout +++ b/cmd/gravity/tests/cmd/instructions.stdout @@ -186,7 +186,7 @@ func (i *InstructionsInstance) U32Roundtrip( val uint32, ) uint32 { arg0 := val - result0 := uint32(arg0) + result0 := api.EncodeU32(arg0) raw1, err1 := i.module.ExportedFunction("u32-roundtrip").Call(ctx, uint64(result0)) // The return type doesn't contain an error so we panic if one is encountered if err1 != nil { diff --git a/cmd/gravity/tests/cmd/records.stdout b/cmd/gravity/tests/cmd/records.stdout index 4745179..64b3920 100644 --- a/cmd/gravity/tests/cmd/records.stdout +++ b/cmd/gravity/tests/cmd/records.stdout @@ -14,17 +14,11 @@ var wasmFileRecords []byte type Foo struct { Float32 float32 - Float64 float64 - Uint32 uint32 - Uint64 uint64 - S string - Vf32 []float32 - Vf64 []float64 } @@ -159,11 +153,13 @@ func (i *RecordsInstance) ModifyFoo( // deferring this, we ensure that no memory is corrupted before the function // is done accessing it. defer func() { - if _, err := i.module.ExportedFunction("cabi_post_modify-foo").Call(ctx, raw10...); 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")) + if postFn := i.module.ExportedFunction("cabi_post_modify-foo"); postFn != nil { + if _, err := postFn.Call(ctx, raw10...); 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")) + } } }() @@ -327,11 +323,13 @@ func (i *RecordsInstance) ModifyFooFallible( // deferring this, we ensure that no memory is corrupted before the function // is done accessing it. defer func() { - if _, err := i.module.ExportedFunction("cabi_post_modify-foo-fallible").Call(ctx, raw10...); 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")) + if postFn := i.module.ExportedFunction("cabi_post_modify-foo-fallible"); postFn != nil { + if _, err := postFn.Call(ctx, raw10...); 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")) + } } }() diff --git a/cmd/gravity/tests/cmd/regressions.stdout b/cmd/gravity/tests/cmd/regressions.stdout index ba00add..7bbafda 100644 --- a/cmd/gravity/tests/cmd/regressions.stdout +++ b/cmd/gravity/tests/cmd/regressions.stdout @@ -257,7 +257,7 @@ func (i *RegressionsInstance) CheckStatus( } results1 := raw1[0] - result2 := uint32(results1) + result2 := api.DecodeU32(uint64(results1)) return result2 } @@ -266,7 +266,7 @@ func (i *RegressionsInstance) DoubleValue( value uint32, ) uint32 { arg0 := value - result0 := uint32(arg0) + result0 := api.EncodeU32(arg0) raw1, err1 := i.module.ExportedFunction("double-value").Call(ctx, uint64(result0)) // The return type doesn't contain an error so we panic if one is encountered if err1 != nil { @@ -274,7 +274,7 @@ func (i *RegressionsInstance) DoubleValue( } results1 := raw1[0] - result2 := uint32(results1) + result2 := api.DecodeU32(uint64(results1)) return result2 } diff --git a/examples/records/Cargo.toml b/examples/records/Cargo.toml index 1ae90a3..a89b627 100644 --- a/examples/records/Cargo.toml +++ b/examples/records/Cargo.toml @@ -7,5 +7,5 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -wit-bindgen = "=0.46.0" -wit-component = "=0.239.0" +wit-bindgen = "=0.53.1" +wit-component = "=0.245.1" From 08b86cc06f4c74ecdeead00efb4a7deca0672a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B6Rei?= Date: Wed, 4 Mar 2026 18:50:17 -0800 Subject: [PATCH 8/8] fix(codegen): use identity casts for I32FromU32, U32FromI32, and I64FromU64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace api.EncodeU32/api.DecodeU32/int64() with simple uint32()/uint64() identity casts. The Encode/Decode helpers round-trip through uint64, which causes Go compile errors when the result is assigned to a uint32 variable (e.g. VariantLower slots) or when int64 is assigned to uint64. The canonical ABI reinterpretation instructions are no-ops at the bit level, so identity casts are correct — wazero only inspects the lower 32 bits for i32 values. Adds regression tests for the variant-payload case that triggered the original compile errors, and TODOs for E2E variant tests once variant type definition generation is implemented. Co-Authored-By: Claude Opus 4.6 --- cmd/gravity/src/codegen/exports.rs | 204 +++++++++++++++++++++- cmd/gravity/src/codegen/func.rs | 52 +++--- cmd/gravity/tests/cmd/instructions.stdout | 4 +- cmd/gravity/tests/cmd/records.stdout | 12 +- cmd/gravity/tests/cmd/regressions.stdout | 6 +- examples/regressions/regressions_test.go | 5 + examples/regressions/wit/regressions.wit | 4 + 7 files changed, 245 insertions(+), 42 deletions(-) diff --git a/cmd/gravity/src/codegen/exports.rs b/cmd/gravity/src/codegen/exports.rs index a4e89ed..a9f42c6 100644 --- a/cmd/gravity/src/codegen/exports.rs +++ b/cmd/gravity/src/codegen/exports.rs @@ -169,7 +169,209 @@ mod tests { assert!(generated.contains("if err1 != nil {")); assert!(generated.contains("panic(err1)")); assert!(generated.contains("results1 := raw1[0]")); - assert!(generated.contains("result2 := api.DecodeU32(uint64(results1))")); + assert!(generated.contains("result2 := uint32(results1)")); assert!(generated.contains("return result2")); + + // I32FromU32 / U32FromI32 are no-op reinterpretations — they must not + // use api.EncodeU32 or api.DecodeU32 (which round-trip through uint64, + // causing type mismatches in VariantLower and needless widening elsewhere). + assert!( + !generated.contains("api.EncodeU32"), + "Export must not use api.EncodeU32 (returns uint64 but downstream expects uint32), got:\n{generated}" + ); + assert!( + !generated.contains("api.DecodeU32"), + "Export must not use api.DecodeU32 (needless uint32→uint64→uint32 round-trip), got:\n{generated}" + ); + } + + /// Regression test: export function with a variant parameter containing + /// a u32 payload must generate Go code where I32FromU32 produces a + /// uint32 value matching the VariantLower variable declaration. + /// Previously I32FromU32 used api.EncodeU32() which returns uint64, + /// causing a Go compile error: cannot use uint64 as uint32. + #[test] + fn test_export_variant_u32_no_encode_u32() { + use wit_bindgen_core::wit_parser::{ + Case, TypeDef, TypeDefKind, TypeOwner, Variant, + }; + + let mut resolve = Resolve::new(); + + // variant u32-option { some-val(u32), none-val } + let variant_def = TypeDef { + name: Some("u32-option".to_string()), + kind: TypeDefKind::Variant(Variant { + cases: vec![ + Case { + name: "some-val".to_string(), + ty: Some(Type::U32), + docs: Default::default(), + span: Default::default(), + }, + Case { + name: "none-val".to_string(), + ty: None, + docs: Default::default(), + span: Default::default(), + }, + ], + }), + owner: TypeOwner::None, + docs: Default::default(), + stability: Default::default(), + span: Default::default(), + }; + let variant_id = resolve.types.alloc(variant_def); + + let func = Function { + name: "process_u32_option".to_string(), + kind: FunctionKind::Freestanding, + params: vec![Param { + name: "opt".to_string(), + ty: Type::Id(variant_id), + span: Default::default(), + }], + result: Some(Type::U32), + docs: Default::default(), + stability: Default::default(), + span: Default::default(), + }; + + let world = World { + name: "test-world".to_string(), + imports: [].into(), + exports: [( + WorldKey::Name("process-u32-option".to_string()), + WorldItem::Function(func.clone()), + )] + .into(), + docs: Default::default(), + stability: Default::default(), + includes: Default::default(), + span: Default::default(), + package: None, + }; + + let mut sizes = SizeAlign::default(); + sizes.fill(&resolve); + let instance = GoIdentifier::public("TestInstance"); + + let config = ExportConfig { + instance: &instance, + world: &world, + resolve: &resolve, + sizes: &sizes, + }; + + let generator = ExportGenerator::new(config); + let mut tokens = Tokens::new(); + generator.generate_function(&func, &mut tokens); + + let generated = tokens.to_string().unwrap(); + println!("Generated u32-option function:\n{}", generated); + + // VariantLower declares `var variant_1 uint32` for the I32 payload slot. + // I32FromU32 must NOT use api.EncodeU32 (returns uint64 → type mismatch) + assert!( + !generated.contains("api.EncodeU32"), + "I32FromU32 must not use api.EncodeU32 in exports (returns uint64, \ + but VariantLower variable is uint32), got:\n{generated}" + ); + } + + /// Regression test: export function with a variant parameter containing + /// a u64 payload must generate Go code where I64FromU64 produces a + /// uint64 value matching the VariantLower variable declaration. + /// Previously I64FromU64 used int64() which returns int64, causing a + /// Go compile error: cannot use int64 as uint64. + #[test] + fn test_export_variant_u64_no_int64_cast() { + use wit_bindgen_core::wit_parser::{ + Case, TypeDef, TypeDefKind, TypeOwner, Variant, + }; + + let mut resolve = Resolve::new(); + + // variant u64-option { some-val(u64), none-val } + let variant_def = TypeDef { + name: Some("u64-option".to_string()), + kind: TypeDefKind::Variant(Variant { + cases: vec![ + Case { + name: "some-val".to_string(), + ty: Some(Type::U64), + docs: Default::default(), + span: Default::default(), + }, + Case { + name: "none-val".to_string(), + ty: None, + docs: Default::default(), + span: Default::default(), + }, + ], + }), + owner: TypeOwner::None, + docs: Default::default(), + stability: Default::default(), + span: Default::default(), + }; + let variant_id = resolve.types.alloc(variant_def); + + let func = Function { + name: "process_u64_option".to_string(), + kind: FunctionKind::Freestanding, + params: vec![Param { + name: "opt".to_string(), + ty: Type::Id(variant_id), + span: Default::default(), + }], + result: Some(Type::U64), + docs: Default::default(), + stability: Default::default(), + span: Default::default(), + }; + + let world = World { + name: "test-world".to_string(), + imports: [].into(), + exports: [( + WorldKey::Name("process-u64-option".to_string()), + WorldItem::Function(func.clone()), + )] + .into(), + docs: Default::default(), + stability: Default::default(), + includes: Default::default(), + span: Default::default(), + package: None, + }; + + let mut sizes = SizeAlign::default(); + sizes.fill(&resolve); + let instance = GoIdentifier::public("TestInstance"); + + let config = ExportConfig { + instance: &instance, + world: &world, + resolve: &resolve, + sizes: &sizes, + }; + + let generator = ExportGenerator::new(config); + let mut tokens = Tokens::new(); + generator.generate_function(&func, &mut tokens); + + let generated = tokens.to_string().unwrap(); + println!("Generated u64-option function:\n{}", generated); + + // VariantLower declares `var variant_1 uint64` for the I64 payload slot. + // I64FromU64 must NOT use int64() (returns int64 → type mismatch) + assert!( + !generated.contains(":= int64("), + "I64FromU64 must not use int64() cast in exports (returns int64, \ + but VariantLower variable is uint64), got:\n{generated}" + ); } } diff --git a/cmd/gravity/src/codegen/func.rs b/cmd/gravity/src/codegen/func.rs index 71e4edf..7dbdfa8 100644 --- a/cmd/gravity/src/codegen/func.rs +++ b/cmd/gravity/src/codegen/func.rs @@ -12,7 +12,7 @@ use crate::{ imports::{ ERRORS_NEW, REFLECT_VALUE_OF, WAZERO_API_DECODE_F32, WAZERO_API_DECODE_F64, WAZERO_API_DECODE_I32, WAZERO_API_DECODE_U32, WAZERO_API_ENCODE_F32, - WAZERO_API_ENCODE_F64, WAZERO_API_ENCODE_I32, WAZERO_API_ENCODE_U32, + WAZERO_API_ENCODE_F64, WAZERO_API_ENCODE_I32, }, GoIdentifier, GoResult, GoType, Operand, }, @@ -314,40 +314,28 @@ impl Bindgen for Func<'_> { let tmp = self.tmp(); let result = &format!("result{tmp}"); let operand = &operands[0]; - match &self.direction { - Direction::Export => { - quote_in! { self.body => - $['\r'] - $result := $WAZERO_API_ENCODE_U32($operand) - }; - } - Direction::Import { .. } => { - quote_in! { self.body => - $['\r'] - $result := uint32($operand) - }; - } - } + // I32FromU32 is a no-op reinterpretation (same 32-bit value, + // different signedness). Use uint32() identity cast in both + // directions — api.EncodeU32 returns uint64 which causes type + // mismatches when assigned to uint32 variables (e.g. VariantLower). + quote_in! { self.body => + $['\r'] + $result := uint32($operand) + }; results.push(Operand::SingleValue(result.into())); } Instruction::U32FromI32 => { + // U32FromI32 is a no-op reinterpretation (same 32-bit value, + // different signedness). Use uint32() identity cast — + // api.DecodeU32(uint64(...)) is a needless round-trip through + // uint64 when the operand is already uint32. let tmp = self.tmp(); let result = &format!("result{tmp}"); let operand = &operands[0]; - match &self.direction { - Direction::Export => { - quote_in! { self.body => - $['\r'] - $result := $WAZERO_API_DECODE_U32(uint64($operand)) - }; - } - Direction::Import { .. } => { - quote_in! { self.body => - $['\r'] - $result := uint32($operand) - }; - } - } + quote_in! { self.body => + $['\r'] + $result := uint32($operand) + }; results.push(Operand::SingleValue(result.into())); } Instruction::PointerLoad { offset } => { @@ -1251,12 +1239,16 @@ impl Bindgen for Func<'_> { } Instruction::I32FromChar => todo!("implement instruction: {inst:?}"), Instruction::I64FromU64 => { + // I64FromU64 is a no-op reinterpretation (same 64-bit value, + // different signedness). Use uint64() identity cast — int64() + // returns int64 which causes type mismatches when assigned to + // uint64 variables (e.g. VariantLower). let tmp = self.tmp(); let value = format!("value{tmp}"); let operand = &operands[0]; quote_in! { self.body => $['\r'] - $(&value) := int64($operand) + $(&value) := uint64($operand) } results.push(Operand::SingleValue(value.into())); } diff --git a/cmd/gravity/tests/cmd/instructions.stdout b/cmd/gravity/tests/cmd/instructions.stdout index 4481ba8..7793b2a 100644 --- a/cmd/gravity/tests/cmd/instructions.stdout +++ b/cmd/gravity/tests/cmd/instructions.stdout @@ -186,7 +186,7 @@ func (i *InstructionsInstance) U32Roundtrip( val uint32, ) uint32 { arg0 := val - result0 := api.EncodeU32(arg0) + result0 := uint32(arg0) raw1, err1 := i.module.ExportedFunction("u32-roundtrip").Call(ctx, uint64(result0)) // The return type doesn't contain an error so we panic if one is encountered if err1 != nil { @@ -194,7 +194,7 @@ func (i *InstructionsInstance) U32Roundtrip( } results1 := raw1[0] - result2 := api.DecodeU32(uint64(results1)) + result2 := uint32(results1) return result2 } diff --git a/cmd/gravity/tests/cmd/records.stdout b/cmd/gravity/tests/cmd/records.stdout index 64b3920..7b114ff 100644 --- a/cmd/gravity/tests/cmd/records.stdout +++ b/cmd/gravity/tests/cmd/records.stdout @@ -106,8 +106,8 @@ func (i *RecordsInstance) ModifyFoo( vf640 := arg0.Vf64 result1 := api.EncodeF32(float320) result2 := api.EncodeF64(float640) - result3 := api.EncodeU32(uint320) - value4 := int64(uint640) + result3 := uint32(uint320) + value4 := uint64(uint640) memory5 := i.module.Memory() realloc5 := i.module.ExportedFunction("cabi_realloc") ptr5, len5, err5 := writeString(ctx, s0, memory5, realloc5) @@ -181,7 +181,7 @@ func (i *RecordsInstance) ModifyFoo( if !ok15 { panic(errors.New("failed to read i32 from memory")) } - result16 := api.DecodeU32(uint64(value15)) + result16 := uint32(value15) value17, ok17 := i.module.Memory().ReadUint64Le(uint32(results10 + 24)) // The return type doesn't contain an error so we panic if one is encountered if !ok17 { @@ -276,8 +276,8 @@ func (i *RecordsInstance) ModifyFooFallible( vf640 := arg0.Vf64 result1 := api.EncodeF32(float320) result2 := api.EncodeF64(float640) - result3 := api.EncodeU32(uint320) - value4 := int64(uint640) + result3 := uint32(uint320) + value4 := uint64(uint640) memory5 := i.module.Memory() realloc5 := i.module.ExportedFunction("cabi_realloc") ptr5, len5, err5 := writeString(ctx, s0, memory5, realloc5) @@ -360,7 +360,7 @@ func (i *RecordsInstance) ModifyFooFallible( var default16 Foo return default16, errors.New("failed to read i32 from memory") } - result17 := api.DecodeU32(uint64(value16)) + result17 := uint32(value16) value18, ok18 := i.module.Memory().ReadUint64Le(uint32(results10 + 32)) if !ok18 { var default18 Foo diff --git a/cmd/gravity/tests/cmd/regressions.stdout b/cmd/gravity/tests/cmd/regressions.stdout index 7bbafda..ba00add 100644 --- a/cmd/gravity/tests/cmd/regressions.stdout +++ b/cmd/gravity/tests/cmd/regressions.stdout @@ -257,7 +257,7 @@ func (i *RegressionsInstance) CheckStatus( } results1 := raw1[0] - result2 := api.DecodeU32(uint64(results1)) + result2 := uint32(results1) return result2 } @@ -266,7 +266,7 @@ func (i *RegressionsInstance) DoubleValue( value uint32, ) uint32 { arg0 := value - result0 := api.EncodeU32(arg0) + result0 := uint32(arg0) raw1, err1 := i.module.ExportedFunction("double-value").Call(ctx, uint64(result0)) // The return type doesn't contain an error so we panic if one is encountered if err1 != nil { @@ -274,7 +274,7 @@ func (i *RegressionsInstance) DoubleValue( } results1 := raw1[0] - result2 := api.DecodeU32(uint64(results1)) + result2 := uint32(results1) return result2 } diff --git a/examples/regressions/regressions_test.go b/examples/regressions/regressions_test.go index 6bdb883..830c2d7 100644 --- a/examples/regressions/regressions_test.go +++ b/examples/regressions/regressions_test.go @@ -158,3 +158,8 @@ func TestRunPing(t *testing.T) { t.Errorf("RunPing() = %v, want true", got) } } + +// TODO: When gravity supports generating Go variant type definitions, add E2E +// tests for export functions that accept variant parameters (e.g. a variant +// with a u32 or u64 payload). These would exercise the VariantLower codepath +// end-to-end through wazero. diff --git a/examples/regressions/wit/regressions.wit b/examples/regressions/wit/regressions.wit index fac5fc1..0401d24 100644 --- a/examples/regressions/wit/regressions.wit +++ b/examples/regressions/wit/regressions.wit @@ -38,4 +38,8 @@ world regressions { export check-status: func(key: string) -> u32; export double-value: func(value: u32) -> u32; export run-ping: func() -> bool; + + // TODO: When variant type definition generation is supported, add variant + // exports here (e.g. variant with u32/u64 payloads) and corresponding E2E + // tests in regressions_test.go. }