From e4d6dbb126839fa83813a0d23e929bd1c4365a12 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 29 Sep 2025 08:55:59 +0100 Subject: [PATCH 1/2] refactor: move various Go syntax related structs to library; add tests This commit moves the various Go syntax related structs to a `go` module in a library, adds unit tests for them, and refactors the `main.rs` file to use the library. It doesn't yet do anything with the 'Func' struct or a couple of others, but it's a step in the direction of a more succinct `main.rs` file. --- cmd/gravity/src/go/comment.rs | 47 ++++ cmd/gravity/src/go/embed.rs | 61 +++++ cmd/gravity/src/go/identifier.rs | 122 ++++++++++ cmd/gravity/src/go/mod.rs | 16 ++ cmd/gravity/src/go/operand.rs | 95 ++++++++ cmd/gravity/src/go/result.rs | 91 ++++++++ cmd/gravity/src/go/type.rs | 268 ++++++++++++++++++++++ cmd/gravity/src/lib.rs | 1 + cmd/gravity/src/main.rs | 376 +++---------------------------- 9 files changed, 728 insertions(+), 349 deletions(-) create mode 100644 cmd/gravity/src/go/comment.rs create mode 100644 cmd/gravity/src/go/embed.rs create mode 100644 cmd/gravity/src/go/identifier.rs create mode 100644 cmd/gravity/src/go/mod.rs create mode 100644 cmd/gravity/src/go/operand.rs create mode 100644 cmd/gravity/src/go/result.rs create mode 100644 cmd/gravity/src/go/type.rs create mode 100644 cmd/gravity/src/lib.rs diff --git a/cmd/gravity/src/go/comment.rs b/cmd/gravity/src/go/comment.rs new file mode 100644 index 0000000..32116ec --- /dev/null +++ b/cmd/gravity/src/go/comment.rs @@ -0,0 +1,47 @@ +use genco::{ + prelude::*, + tokens::{ItemStr, static_literal}, +}; + +/// Format a comment where each line is preceeded by `//`. +/// Based on https://github.com/udoprog/genco/blob/1ec4869f458cf71d1d2ffef77fe051ea8058b391/src/lang/csharp/comment.rs +pub struct Comment(T); + +impl FormatInto for Comment +where + T: IntoIterator, + T::Item: Into, +{ + fn format_into(self, tokens: &mut Tokens) { + for line in self.0 { + tokens.push(); + tokens.append(static_literal("//")); + tokens.space(); + tokens.append(line.into()); + } + } +} + +/// Helper function to create a Go comment. +pub fn comment(comment: T) -> Comment +where + T: IntoIterator, + T::Item: Into, +{ + Comment(comment) +} + +#[cfg(test)] +mod tests { + use genco::{prelude::*, tokens::Tokens}; + + use crate::go::comment; + + #[test] + fn test_comment() { + let comment = comment(&["hello", "world"]); + let mut tokens = Tokens::::new(); + comment.format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "// hello\n// world"); + } +} diff --git a/cmd/gravity/src/go/embed.rs b/cmd/gravity/src/go/embed.rs new file mode 100644 index 0000000..497c760 --- /dev/null +++ b/cmd/gravity/src/go/embed.rs @@ -0,0 +1,61 @@ +use genco::{ + prelude::*, + tokens::{ItemStr, static_literal}, +}; + +/// Type for generating Go embed directives (//go:embed) +pub struct Embed(pub T); + +impl FormatInto for Embed +where + T: Into, +{ + fn format_into(self, tokens: &mut Tokens) { + // TODO(#13): Submit patch to genco that will allow aliases for go imports + // tokens.register(go::import("embed", "")); + tokens.push(); + tokens.append(static_literal("//go:embed")); + tokens.space(); + tokens.append(self.0.into()); + } +} + +/// Helper function to create an embed directive. +pub fn embed(path: T) -> Embed +where + T: Into, +{ + Embed(path) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_embed_directive() { + let mut tokens = Tokens::::new(); + let embed = embed("module.wasm"); + + quote_in! { tokens => + $(embed) + }; + + let output = tokens.to_string().unwrap(); + assert!(output.contains("//go:embed module.wasm")); + } + + #[test] + fn test_embed_with_variable() { + let mut tokens = Tokens::::new(); + + quote_in! { tokens => + $(embed("app.wasm")) + var wasmFile []byte + }; + + let output = tokens.to_string().unwrap(); + assert!(output.contains("//go:embed app.wasm")); + assert!(output.contains("var wasmFile []byte")); + } +} diff --git a/cmd/gravity/src/go/identifier.rs b/cmd/gravity/src/go/identifier.rs new file mode 100644 index 0000000..4e0c7a2 --- /dev/null +++ b/cmd/gravity/src/go/identifier.rs @@ -0,0 +1,122 @@ +use std::str::Chars; + +use genco::{prelude::*, tokens::ItemStr}; + +/// Represents a Go identifier with appropriate casing rules. +/// +/// Go identifiers follow specific naming conventions: +/// - Public identifiers start with uppercase (exported) +/// - Private identifiers start with lowercase (unexported) +/// - Local identifiers are used as-is without transformation +#[derive(Debug, Clone, Copy)] +pub enum GoIdentifier<'a> { + /// Public/exported identifier (will be converted to UpperCamelCase) + Public { name: &'a str }, + /// Private/unexported identifier (will be converted to lowerCamelCase) + Private { name: &'a str }, + /// Local identifier (will be converted to lowerCamelCase) + Local { name: &'a str }, +} + +impl<'a> GoIdentifier<'a> { + /// Creates a new public identifier. + pub fn public(name: &'a str) -> Self { + Self::Public { name } + } + + /// Creates a new private identifier. + pub fn private(name: &'a str) -> Self { + Self::Private { name } + } + + /// Creates a new local identifier. + pub fn local<'b: 'a>(name: &'a str) -> Self { + Self::Local { name } + } + + /// Returns an iterator over the characters of the underlying name. + /// + /// This provides access to the raw name without case transformations. + /// + /// # Returns + /// An iterator over the characters of the identifier's name. + pub fn chars(&self) -> Chars<'a> { + match self { + GoIdentifier::Public { name } => name.chars(), + GoIdentifier::Private { name } => name.chars(), + GoIdentifier::Local { name } => name.chars(), + } + } +} + +impl From> for String { + fn from(value: GoIdentifier) -> Self { + let mut tokens: Tokens = Tokens::new(); + value.format_into(&mut tokens); + tokens.to_string().expect("to format correctly") + } +} + +impl FormatInto for &GoIdentifier<'_> { + fn format_into(self, tokens: &mut Tokens) { + let mut chars = self.chars(); + + // TODO(#12): Check for invalid first character + + if let GoIdentifier::Public { .. } = self { + // https://stackoverflow.com/a/38406885 + match chars.next() { + Some(c) => tokens.append(ItemStr::from(c.to_uppercase().to_string())), + None => panic!("No function name"), + }; + }; + + while let Some(c) = chars.next() { + match c { + ' ' | '-' | '_' => { + if let Some(c) = chars.next() { + tokens.append(ItemStr::from(c.to_uppercase().to_string())); + } + } + _ => tokens.append(ItemStr::from(c.to_string())), + } + } + } +} +impl FormatInto for GoIdentifier<'_> { + fn format_into(self, tokens: &mut Tokens) { + (&self).format_into(tokens) + } +} + +#[cfg(test)] +mod tests { + + use genco::{prelude::*, tokens::Tokens}; + + use crate::go::GoIdentifier; + + #[test] + fn test_public_identifier() { + let id = GoIdentifier::public("hello-world"); + let mut tokens = Tokens::::new(); + (&id).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "HelloWorld"); + } + + #[test] + fn test_private_identifier() { + let id = GoIdentifier::private("hello-world"); + let mut tokens = Tokens::::new(); + (&id).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "helloWorld"); + } + + #[test] + fn test_local_identifier() { + let id = GoIdentifier::local("hello-world"); + let mut tokens = Tokens::::new(); + (&id).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "helloWorld"); + } +} diff --git a/cmd/gravity/src/go/mod.rs b/cmd/gravity/src/go/mod.rs new file mode 100644 index 0000000..96b1eea --- /dev/null +++ b/cmd/gravity/src/go/mod.rs @@ -0,0 +1,16 @@ +//! Representations of Go types, and implementations for formatting them. + +mod comment; +mod embed; +#[path = "./type.rs"] +mod go_type; +mod identifier; +mod operand; +mod result; + +pub use comment::*; +pub use embed::*; +pub use go_type::*; +pub use identifier::*; +pub use operand::*; +pub use result::*; diff --git a/cmd/gravity/src/go/operand.rs b/cmd/gravity/src/go/operand.rs new file mode 100644 index 0000000..60d7061 --- /dev/null +++ b/cmd/gravity/src/go/operand.rs @@ -0,0 +1,95 @@ +use genco::{ + prelude::*, + tokens::{ItemStr, static_literal}, +}; + +/// Represents an operand in Go code generation. +/// +/// Operands can be literals, single values (variables), or multi-value tuples +/// (used for functions returning multiple values). +#[derive(Debug, Clone, PartialEq)] +pub enum Operand { + /// A literal value (e.g., "0", "true", "\"hello\"") + Literal(String), + /// A single variable or expression + SingleValue(String), + /// A tuple of two values (for multi-value returns) + MultiValue((String, String)), +} + +impl Operand { + /// Returns the primary value of the operand. + /// + /// For single values and literals, returns the value itself. + /// For multi-value tuples, returns the first value. + /// + /// # Returns + /// A string representation of the primary value. + pub fn as_string(&self) -> String { + match self { + Operand::Literal(s) => s.clone(), + Operand::SingleValue(s) => s.clone(), + Operand::MultiValue((s1, _)) => s1.clone(), + } + } +} + +// Implement genco's FormatInto for Operand so it can be used in quote! macros +impl FormatInto for &Operand { + fn format_into(self, tokens: &mut Tokens) { + match self { + Operand::Literal(val) => tokens.append(ItemStr::from(val)), + Operand::SingleValue(val) => tokens.append(ItemStr::from(val)), + Operand::MultiValue((val1, val2)) => { + tokens.append(ItemStr::from(val1)); + tokens.append(static_literal(",")); + tokens.space(); + tokens.append(ItemStr::from(val2)); + } + } + } +} + +impl FormatInto for Operand { + fn format_into(self, tokens: &mut Tokens) { + (&self).format_into(tokens) + } +} + +impl FormatInto for &mut Operand { + fn format_into(self, tokens: &mut Tokens) { + let op: &Operand = self; + op.format_into(tokens) + } +} + +#[cfg(test)] +mod tests { + use genco::{prelude::*, tokens::Tokens}; + + use crate::go::Operand; + + #[test] + fn test_operand_literal() { + let op = Operand::Literal("42".to_string()); + let mut tokens = Tokens::::new(); + op.format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "42"); + } + + #[test] + fn test_operand_single_value() { + let op = Operand::SingleValue("myVar".to_string()); + let mut tokens = Tokens::::new(); + op.format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "myVar"); + } + + #[test] + fn test_operand_multi_value() { + let op = Operand::MultiValue(("val1".to_string(), "val2".to_string())); + let mut tokens = Tokens::::new(); + op.format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "val1, val2"); + } +} diff --git a/cmd/gravity/src/go/result.rs b/cmd/gravity/src/go/result.rs new file mode 100644 index 0000000..102f01b --- /dev/null +++ b/cmd/gravity/src/go/result.rs @@ -0,0 +1,91 @@ +use genco::prelude::*; + +use crate::go::GoType; + +/// Represents a Go function result type. +/// +/// Can be either empty (no return value) or an anonymous type. +/// Used for modeling function returns in the generated Go code. +#[derive(Debug, Clone, PartialEq)] +pub enum GoResult { + /// No return value + Empty, + /// Anonymous return type + Anon(GoType), +} + +impl GoResult { + /// Returns true if this result type needs post-return cleanup. + /// + /// Delegates to the underlying type's cleanup requirements. + /// Empty results don't need cleanup as they represent no value. + /// + /// # Returns + /// `true` if cleanup is needed, `false` otherwise. + pub fn needs_cleanup(&self) -> bool { + match self { + GoResult::Empty => false, + GoResult::Anon(typ) => typ.needs_cleanup(), + } + } +} + +impl FormatInto for GoResult { + fn format_into(self, tokens: &mut Tokens) { + (&self).format_into(tokens) + } +} + +impl FormatInto for &GoResult { + fn format_into(self, tokens: &mut Tokens) { + match &self { + GoResult::Anon(typ @ GoType::ValueOrError(_) | typ @ GoType::ValueOrOk(_)) => { + // Be cautious here as there are `(` and `)` surrounding the type + tokens.append(quote!(($typ))) + } + GoResult::Anon(typ) => typ.format_into(tokens), + GoResult::Empty => (), + } + } +} + +#[cfg(test)] +mod tests { + use genco::{prelude::*, tokens::Tokens}; + + use crate::go::{GoResult, GoType}; + + #[test] + fn test_go_result_empty() { + let result = GoResult::Empty; + let mut tokens = Tokens::::new(); + (&result).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), ""); + } + + #[test] + fn test_go_result_simple_type() { + let result = GoResult::Anon(GoType::String); + let mut tokens = Tokens::::new(); + (&result).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "string"); + } + + #[test] + fn test_go_result_value_or_ok() { + // GoResult with ValueOrOk should add parentheses + let result = GoResult::Anon(GoType::ValueOrOk(Box::new(GoType::Uint32))); + let mut tokens = Tokens::::new(); + (&result).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "(uint32, bool)"); + } + + #[test] + fn test_go_result_value_or_error() { + // GoResult with ValueOrError should add parentheses + let result = GoResult::Anon(GoType::ValueOrError(Box::new(GoType::String))); + let mut tokens = Tokens::::new(); + (&result).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "(string, error)"); + } +} diff --git a/cmd/gravity/src/go/type.rs b/cmd/gravity/src/go/type.rs new file mode 100644 index 0000000..47cfaba --- /dev/null +++ b/cmd/gravity/src/go/type.rs @@ -0,0 +1,268 @@ +use genco::{prelude::*, tokens::static_literal}; + +use crate::go::GoIdentifier; + +/// Represents a Go type in the code generation system. +/// +/// This enum covers all the basic Go types as well as special types +/// used for WebAssembly Component Model interop. +#[derive(Debug, Clone, PartialEq)] +pub enum GoType { + /// Boolean type + Bool, + /// Unsigned 8-bit integer + Uint8, + /// Unsigned 16-bit integer + Uint16, + /// Unsigned 32-bit integer + Uint32, + /// Unsigned 64-bit integer + Uint64, + /// Signed 8-bit integer + Int8, + /// Signed 16-bit integer + Int16, + /// Signed 32-bit integer + Int32, + /// Signed 64-bit integer + Int64, + /// 32-bit floating point + Float32, + /// 64-bit floating point + Float64, + /// String type + String, + /// Error type (represents Result) + Error, + /// Interface type (for variants/discriminated unions) + Interface, + // Pointer to another type + // Pointer(Box), + /// Result type with Ok value + ValueOrOk(Box), + /// Result type with Error value + ValueOrError(Box), + /// Slice/array of another type + Slice(Box), + /// Multi-return type (for functions returning arbitrary multiple values) + // MultiReturn(Vec), + /// User-defined type (records, enums, type aliases) + UserDefined(String), + /// Represents no value/void + Nothing, +} + +impl GoType { + /// Returns true if this type needs post-return cleanup (cabi_post_* function) + /// + /// According to the Component Model Canonical ABI specification, cleanup is needed + /// for types that allocate memory in the guest's linear memory when being returned. + /// + /// Types that need cleanup: + /// - Strings: allocate memory for the string data + /// - Lists/Slices: allocate memory for the array data + /// - Types containing the above (recursively) + /// + /// Types that DON'T need cleanup: + /// - Primitives (bool, integers, floats): passed by value + /// - Enums: represented as integers + /// + /// Limitations: + /// - For UserDefined types (records, type aliases), we can't determine here if they + /// contain strings/lists without the full type definition, so we're conservative + /// - A perfect implementation would recursively check record fields, but that would + /// require passing the Resolve context here + pub fn needs_cleanup(&self) -> bool { + match self { + // Primitive types don't need cleanup + GoType::Bool + | GoType::Uint8 + | GoType::Uint16 + | GoType::Uint32 + | GoType::Uint64 + | GoType::Int8 + | GoType::Int16 + | GoType::Int32 + | GoType::Int64 + | GoType::Float32 + | GoType::Float64 => false, + + // String and slices allocate memory and need cleanup + GoType::String | GoType::Slice(_) => true, + + // Complex types need cleanup if their inner types do + GoType::ValueOrOk(inner) => inner.needs_cleanup(), + + // The inner type of `Err` is always a String so it requires cleanup + // TODO(#91): Store the error type to check both inner types. + GoType::ValueOrError(_) => true, + + // Interfaces (variants) might need cleanup (conservative approach) + GoType::Interface => true, + + // User-defined types (records, enums, type aliases) need cleanup if they + // contain strings or other allocated types. Since we don't have access to + // the type definition here, we must be conservative and assume they might. + // + // This means we might generate unnecessary cleanup calls for: + // - Enums (which are just integers) + // - Records containing only primitives + // - Type aliases to primitives + // + // TODO(#92): Improve this by either: + // 1. Passing the Resolve context to check actual type definitions + // 2. Tracking cleanup requirements during type resolution + // 3. Using a different representation that carries this information + GoType::UserDefined(_) => true, + + // Error is actually Result - strings need cleanup! + GoType::Error => true, + + // Nothing represents no value, so no cleanup needed + GoType::Nothing => false, + // TODO - figure out if a pointer needs cleanup, once implemented. + // GoType::Pointer(_) => todo!() + } + } +} + +impl FormatInto for &GoType { + fn format_into(self, tokens: &mut Tokens) { + match self { + GoType::Bool => tokens.append(static_literal("bool")), + GoType::Uint8 => tokens.append(static_literal("uint8")), + GoType::Uint16 => tokens.append(static_literal("uint16")), + GoType::Uint32 => tokens.append(static_literal("uint32")), + GoType::Uint64 => tokens.append(static_literal("uint64")), + GoType::Int8 => tokens.append(static_literal("int8")), + GoType::Int16 => tokens.append(static_literal("int16")), + GoType::Int32 => tokens.append(static_literal("int32")), + GoType::Int64 => tokens.append(static_literal("int64")), + GoType::Float32 => tokens.append(static_literal("float32")), + GoType::Float64 => tokens.append(static_literal("float64")), + GoType::String => tokens.append(static_literal("string")), + GoType::Error => tokens.append(static_literal("error")), + GoType::Interface => tokens.append(static_literal("interface{}")), + GoType::ValueOrOk(value_typ) => { + value_typ.as_ref().format_into(tokens); + tokens.append(static_literal(",")); + tokens.space(); + tokens.append(static_literal("bool")) + } + GoType::ValueOrError(value_typ) => { + value_typ.as_ref().format_into(tokens); + tokens.append(static_literal(",")); + tokens.space(); + tokens.append(static_literal("error")) + } + GoType::Slice(typ) => { + tokens.append(static_literal("[]")); + typ.as_ref().format_into(tokens); + } + // GoType::MultiReturn(typs) => { + // tokens.append(quote!($(for typ in typs join (, ) => $typ))) + // } + // GoType::Pointer(typ) => { + // tokens.append(static_literal("*")); + // typ.as_ref().format_into(tokens); + // } + GoType::UserDefined(name) => { + let id = GoIdentifier::public(name); + id.format_into(tokens) + } + GoType::Nothing => (), + } + } +} + +impl FormatInto for GoType { + fn format_into(self, tokens: &mut Tokens) { + (&self).format_into(tokens) + } +} + +#[cfg(test)] +mod tests { + use genco::{prelude::*, tokens::Tokens}; + + use crate::go::GoType; + + #[test] + fn test_basic_types() { + let cases = vec![ + (GoType::Bool, "bool"), + (GoType::Uint8, "uint8"), + (GoType::Uint16, "uint16"), + (GoType::Uint32, "uint32"), + (GoType::Uint64, "uint64"), + (GoType::Int8, "int8"), + (GoType::Int16, "int16"), + (GoType::Int32, "int32"), + (GoType::Int64, "int64"), + (GoType::Float32, "float32"), + (GoType::Float64, "float64"), + (GoType::String, "string"), + (GoType::Error, "error"), + (GoType::Interface, "interface{}"), + (GoType::Nothing, ""), + ]; + + for (typ, expected) in cases { + let mut tokens = Tokens::::new(); + (&typ).format_into(&mut tokens); + assert_eq!( + tokens.to_string().unwrap(), + expected, + "Failed for type: {:?}", + typ + ); + } + } + + #[test] + fn test_value_or_ok() { + let typ = GoType::ValueOrOk(Box::new(GoType::Uint32)); + let mut tokens = Tokens::::new(); + (&typ).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "uint32, bool"); + } + + #[test] + fn test_value_or_error() { + let typ = GoType::ValueOrError(Box::new(GoType::String)); + let mut tokens = Tokens::::new(); + (&typ).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "string, error"); + } + + #[test] + fn test_slice() { + let typ = GoType::Slice(Box::new(GoType::Int32)); + let mut tokens = Tokens::::new(); + (&typ).format_into(&mut tokens); + assert_eq!(tokens.to_string().unwrap(), "[]int32"); + } + + // #[test] + // fn test_pointer() { + // let typ = GoType::Pointer(Box::new(GoType::String)); + // let mut tokens = Tokens::::new(); + // (&typ).format_into(&mut tokens); + // assert_eq!(tokens.to_string().unwrap(), "*string"); + // } + + // #[test] + // fn test_nested_types() { + // // Test *[]string + // let typ = GoType::Pointer(Box::new(GoType::Slice(Box::new(GoType::String)))); + // let mut tokens = Tokens::::new(); + // (&typ).format_into(&mut tokens); + // assert_eq!(tokens.to_string().unwrap(), "*[]string"); + + // // Test [][]uint8 + // let typ = GoType::Slice(Box::new(GoType::Slice(Box::new(GoType::Uint8)))); + // let mut tokens = Tokens::::new(); + // (&typ).format_into(&mut tokens); + // assert_eq!(tokens.to_string().unwrap(), "[][]uint8"); + // } +} diff --git a/cmd/gravity/src/lib.rs b/cmd/gravity/src/lib.rs new file mode 100644 index 0000000..5e22f16 --- /dev/null +++ b/cmd/gravity/src/lib.rs @@ -0,0 +1 @@ +pub mod go; diff --git a/cmd/gravity/src/main.rs b/cmd/gravity/src/main.rs index f8eb757..dfea970 100644 --- a/cmd/gravity/src/main.rs +++ b/cmd/gravity/src/main.rs @@ -1,11 +1,11 @@ -use std::{collections::BTreeMap, fs, mem, path::Path, process::ExitCode, str::Chars}; +use std::{collections::BTreeMap, fs, mem, path::Path, process::ExitCode}; use clap::{Arg, ArgAction, Command}; use genco::{ Tokens, lang::{Go, go}, quote, quote_in, - tokens::{FormatInto, ItemStr, quoted, static_literal}, + tokens::{FormatInto, quoted}, }; use wit_bindgen_core::{ abi::{AbiVariant, Bindgen, Instruction, LiftLower, WasmType}, @@ -15,240 +15,8 @@ use wit_bindgen_core::{ }, }; -struct Embed(T); -impl FormatInto for Embed -where - T: Into, -{ - fn format_into(self, tokens: &mut Tokens) { - // TODO(#13): Submit patch to genco that will allow aliases for go imports - // tokens.register(go::import("embed", "")); - tokens.push(); - tokens.append(static_literal("//go:embed")); - tokens.space(); - tokens.append(self.0.into()); - } -} - -fn go_embed(comment: T) -> Embed -where - T: Into, -{ - Embed(comment) -} - -// Format a comment where each line is preceeded by `//`. -// Based on https://github.com/udoprog/genco/blob/1ec4869f458cf71d1d2ffef77fe051ea8058b391/src/lang/csharp/comment.rs -struct Comment(T); - -impl FormatInto for Comment -where - T: IntoIterator, - T::Item: Into, -{ - fn format_into(self, tokens: &mut Tokens) { - for line in self.0 { - tokens.push(); - tokens.append(static_literal("//")); - tokens.space(); - tokens.append(line.into()); - } - } -} - -fn comment(comment: T) -> Comment -where - T: IntoIterator, - T::Item: Into, -{ - Comment(comment) -} - -#[derive(Debug, Clone)] -enum GoType { - Bool, - Uint8, - Uint16, - Uint32, - Uint64, - Int8, - Int16, - Int32, - Int64, - Float32, - Float64, - String, - Error, - Interface, - // Pointer(Box), - ValueOrOk(Box), - ValueOrError(Box), - Slice(Box), - // MultiReturn(Vec), - UserDefined(String), - Nothing, -} - -impl GoType { - /// Returns true if this type needs post-return cleanup (cabi_post_* function) - /// - /// According to the Component Model Canonical ABI specification, cleanup is needed - /// for types that allocate memory in the guest's linear memory when being returned. - /// - /// Types that need cleanup: - /// - Strings: allocate memory for the string data - /// - Lists/Slices: allocate memory for the array data - /// - Types containing the above (recursively) - /// - /// Types that DON'T need cleanup: - /// - Primitives (bool, integers, floats): passed by value - /// - Enums: represented as integers - /// - /// Limitations: - /// - For UserDefined types (records, type aliases), we can't determine here if they - /// contain strings/lists without the full type definition, so we're conservative - /// - A perfect implementation would recursively check record fields, but that would - /// require passing the Resolve context here - fn needs_cleanup(&self) -> bool { - match self { - // Primitive types don't need cleanup - GoType::Bool - | GoType::Uint8 - | GoType::Uint16 - | GoType::Uint32 - | GoType::Uint64 - | GoType::Int8 - | GoType::Int16 - | GoType::Int32 - | GoType::Int64 - | GoType::Float32 - | GoType::Float64 => false, - - // String and slices allocate memory and need cleanup - GoType::String | GoType::Slice(_) => true, - - // Complex types need cleanup if their inner types do - GoType::ValueOrOk(inner) => inner.needs_cleanup(), - - // The inner type of `Err` is always a String so it requires cleanup - // TODO(#91): Store the error type to check both inner types. - GoType::ValueOrError(_) => true, - - // Interfaces (variants) might need cleanup (conservative approach) - GoType::Interface => true, - - // User-defined types (records, enums, type aliases) need cleanup if they - // contain strings or other allocated types. Since we don't have access to - // the type definition here, we must be conservative and assume they might. - // - // This means we might generate unnecessary cleanup calls for: - // - Enums (which are just integers) - // - Records containing only primitives - // - Type aliases to primitives - // - // TODO(#92): Improve this by either: - // 1. Passing the Resolve context to check actual type definitions - // 2. Tracking cleanup requirements during type resolution - // 3. Using a different representation that carries this information - GoType::UserDefined(_) => true, - - // Error is actually Result - strings need cleanup! - GoType::Error => true, - - // Nothing represents no value, so no cleanup needed - GoType::Nothing => false, - } - } -} - -impl FormatInto for &GoType { - fn format_into(self, tokens: &mut Tokens) { - match self { - GoType::Bool => tokens.append(static_literal("bool")), - GoType::Uint8 => tokens.append(static_literal("uint8")), - GoType::Uint16 => tokens.append(static_literal("uint16")), - GoType::Uint32 => tokens.append(static_literal("uint32")), - GoType::Uint64 => tokens.append(static_literal("uint64")), - GoType::Int8 => tokens.append(static_literal("int8")), - GoType::Int16 => tokens.append(static_literal("int16")), - GoType::Int32 => tokens.append(static_literal("int32")), - GoType::Int64 => tokens.append(static_literal("int64")), - GoType::Float32 => tokens.append(static_literal("float32")), - GoType::Float64 => tokens.append(static_literal("float64")), - GoType::String => tokens.append(static_literal("string")), - GoType::Error => tokens.append(static_literal("error")), - GoType::Interface => tokens.append(static_literal("interface{}")), - GoType::ValueOrOk(value_typ) => { - value_typ.as_ref().format_into(tokens); - tokens.append(static_literal(",")); - tokens.space(); - tokens.append(static_literal("bool")) - } - GoType::ValueOrError(value_typ) => { - value_typ.as_ref().format_into(tokens); - tokens.append(static_literal(",")); - tokens.space(); - tokens.append(static_literal("error")) - } - GoType::Slice(typ) => { - tokens.append(static_literal("[]")); - typ.as_ref().format_into(tokens); - } - // GoType::MultiReturn(typs) => { - // tokens.append(quote!($(for typ in typs join (, ) => $typ))) - // } - // GoType::Pointer(typ) => { - // tokens.append(static_literal("*")); - // typ.as_ref().format_into(tokens); - // } - GoType::UserDefined(name) => { - let id = GoIdentifier::Public { name }; - id.format_into(tokens) - } - GoType::Nothing => (), - } - } -} - -impl FormatInto for GoType { - fn format_into(self, tokens: &mut Tokens) { - (&self).format_into(tokens) - } -} - -#[derive(Clone)] -enum GoResult { - Empty, - Anon(GoType), -} - -impl GoResult { - /// Returns true if this result type needs post-return cleanup - fn needs_cleanup(&self) -> bool { - match self { - GoResult::Empty => false, - GoResult::Anon(typ) => typ.needs_cleanup(), - } - } -} +use arcjet_gravity::go::{GoIdentifier, GoResult, GoType, Operand, comment, embed}; -impl FormatInto for GoResult { - fn format_into(self, tokens: &mut Tokens) { - (&self).format_into(tokens) - } -} -impl FormatInto for &GoResult { - fn format_into(self, tokens: &mut Tokens) { - match &self { - GoResult::Anon(typ @ GoType::ValueOrError(_) | typ @ GoType::ValueOrOk(_)) => { - // Be cautious here as there are `(` and `)` surrounding the type - tokens.append(quote!(($typ))) - } - GoResult::Anon(typ) => typ.format_into(tokens), - GoResult::Empty => (), - } - } -} enum Direction { Export, Import { interface_name: String }, @@ -265,64 +33,6 @@ struct Func { sizes: SizeAlign, } -#[derive(Clone, Copy)] -enum GoIdentifier<'a> { - Public { name: &'a str }, - Private { name: &'a str }, - Local { name: &'a str }, -} - -impl<'a> GoIdentifier<'a> { - fn chars(&self) -> Chars<'a> { - match self { - GoIdentifier::Public { name } => name.chars(), - GoIdentifier::Private { name } => name.chars(), - GoIdentifier::Local { name } => name.chars(), - } - } -} - -impl From> for String { - fn from(value: GoIdentifier) -> Self { - let mut tokens: Tokens = Tokens::new(); - value.format_into(&mut tokens); - tokens.to_string().expect("to format correctly") - } -} - -impl FormatInto for &GoIdentifier<'_> { - fn format_into(self, tokens: &mut Tokens) { - let mut chars = self.chars(); - - // TODO(#12): Check for invalid first character - - if let GoIdentifier::Public { .. } = self { - // https://stackoverflow.com/a/38406885 - match chars.next() { - Some(c) => tokens.append(ItemStr::from(c.to_uppercase().to_string())), - None => panic!("No function name"), - }; - }; - - while let Some(c) = chars.next() { - match c { - ' ' | '-' | '_' => { - if let Some(c) = chars.next() { - tokens.append(ItemStr::from(c.to_uppercase().to_string())); - } - } - _ => tokens.append(ItemStr::from(c.to_string())), - } - } - } -} - -impl FormatInto for GoIdentifier<'_> { - fn format_into(self, tokens: &mut Tokens) { - (&self).format_into(tokens) - } -} - impl Func { fn export(result: GoResult, sizes: SizeAlign) -> Self { Self { @@ -379,34 +89,6 @@ impl FormatInto for Func { } } -#[derive(Debug, Clone)] -enum Operand { - Literal(String), - SingleValue(String), - MultiValue((String, String)), -} - -impl FormatInto for &Operand { - fn format_into(self, tokens: &mut Tokens) { - match self { - Operand::Literal(val) => tokens.append(ItemStr::from(val)), - Operand::SingleValue(val) => tokens.append(ItemStr::from(val)), - Operand::MultiValue((val1, val2)) => { - tokens.append(ItemStr::from(val1)); - tokens.append(static_literal(",")); - tokens.space(); - tokens.append(ItemStr::from(val2)); - } - } - } -} -impl FormatInto for &mut Operand { - fn format_into(self, tokens: &mut Tokens) { - let op: &Operand = self; - op.format_into(tokens) - } -} - impl Bindgen for Func { type Operand = Operand; @@ -846,7 +528,7 @@ impl Bindgen for Func { } } Instruction::CallInterface { func, .. } => { - let ident = GoIdentifier::Public { name: &func.name }; + let ident = GoIdentifier::public(&func.name); let tmp = self.tmp(); let args = quote!($(for op in operands.iter() join (, ) => $op)); let returns = match &func.result { @@ -859,9 +541,7 @@ impl Bindgen for Func { match &self.direction { Direction::Export { .. } => todo!("TODO(#10): handle export direction"), Direction::Import { interface_name, .. } => { - let iface = GoIdentifier::Local { - name: interface_name, - }; + let iface = GoIdentifier::local(interface_name); quote_in! { self.body => $['\r'] $(match returns { @@ -1174,7 +854,7 @@ impl Bindgen for Func { let tmp = self.tmp(); let operand = &operands[0]; for field in record.fields.iter() { - let struct_field = GoIdentifier::Public { name: &field.name }; + let struct_field = GoIdentifier::public(&field.name); let var = GoIdentifier::Local { name: &format!("{}{tmp}", &field.name), }; @@ -1192,11 +872,11 @@ impl Bindgen for Func { .fields .iter() .zip(operands) - .map(|(field, op)| (GoIdentifier::Public { name: &field.name }, op)); + .map(|(field, op)| (GoIdentifier::public(&field.name), op)); quote_in! {self.body => $['\r'] - $value := $(GoIdentifier::Public { name }){ + $value := $(GoIdentifier::public(name)){ $(for (name, op) in fields join ($['\r']) => $name: $op,) } }; @@ -1319,7 +999,7 @@ impl Bindgen for Func { }; } - let name = GoIdentifier::Public { name: &case.name }; + let name = GoIdentifier::public(&case.name); quote_in! { cases => $['\r'] case $name: @@ -1356,7 +1036,7 @@ impl Bindgen for Func { let mut cases: Tokens = Tokens::new(); for (i, case) in enum_.cases.iter().enumerate() { - let case_name = GoIdentifier::Public { name: &case.name }; + let case_name = GoIdentifier::public(&case.name); quote_in! { cases => $['\r'] case $case_name: @@ -1580,12 +1260,10 @@ impl Bindings { let TypeDef { name, kind, .. } = typ_def; match kind { TypeDefKind::Record(Record { fields }) => { - let name = GoIdentifier::Public { - name: &name.clone().expect("record to have a name"), - }; + let name = GoIdentifier::public(name.as_deref().expect("record to have a name")); let fields = fields.iter().map(|field| { ( - GoIdentifier::Public { name: &field.name }, + GoIdentifier::public(&field.name), resolve_type(&field.ty, resolve), ) }); @@ -1606,17 +1284,18 @@ impl Bindings { } TypeDefKind::Enum(inner) => { let name = name.clone().expect("enum to have a name"); - let enum_type = GoIdentifier::Private { name: &name }; + let enum_type = GoIdentifier::private(&name); - let enum_interface = GoIdentifier::Public { name: &name }; + let enum_interface = GoIdentifier::public(&name); let enum_function = GoIdentifier::Private { name: &format!("is-{}", &name), }; - let variants = inner.cases.iter().map(|variant| GoIdentifier::Public { - name: &variant.name, - }); + let variants = inner + .cases + .iter() + .map(|variant| GoIdentifier::public(&variant.name)); quote_in! { self.out => $['\n'] @@ -1654,9 +1333,8 @@ impl Bindings { TypeDefKind::Type(Type::F64) => todo!("TODO(#4): generate f64 type alias"), TypeDefKind::Type(Type::Char) => todo!("TODO(#4): generate char type alias"), TypeDefKind::Type(Type::String) => { - let name = GoIdentifier::Public { - name: &name.clone().expect("string alias to have a name"), - }; + let name = + GoIdentifier::public(name.as_deref().expect("string alias to have a name")); // TODO(#4): We might want a Type Definition (newtype) instead of Type Alias here quote_in! { self.out => $['\n'] @@ -1778,7 +1456,7 @@ fn main() -> Result { quote_in! { bindings.out => import _ "embed" - $(go_embed(wasm_file)) + $(embed(wasm_file)) var $raw_wasm []byte } } @@ -1833,7 +1511,7 @@ fn main() -> Result { let mut params = Vec::with_capacity(func.params.len()); for (name, wit_type) in func.params.iter() { let go_type = resolve_type(wit_type, &bindgen.resolve); - params.push((GoIdentifier::Local { name }, go_type)); + params.push((GoIdentifier::local(name), go_type)); } let result = match func.result { @@ -1844,7 +1522,7 @@ fn main() -> Result { None => GoResult::Empty, }; - let func_name = GoIdentifier::Public { name: &func.name }; + let func_name = GoIdentifier::public(&func.name); quote_in! { interface_funcs => $['\r'] $(&func_name)( @@ -1943,7 +1621,7 @@ fn main() -> Result { $['\n'] func $new_factory( ctx $context, - $(for interface_name in ifaces.iter() join ($['\r']) => $(GoIdentifier::Local { name: interface_name }) $(GoIdentifier::Public { + $(for interface_name in ifaces.iter() join ($['\r']) => $(GoIdentifier::local(interface_name)) $(GoIdentifier::Public { name: &format!("i-{selected_world}-{interface_name}"), }),) ) (*$factory, error) { @@ -2030,9 +1708,9 @@ fn main() -> Result { // We can't represent this as an argument type so we unwrap the Some type // TODO: Figure out a better way to handle this GoType::ValueOrOk(typ) => { - params.push((GoIdentifier::Local { name }, *typ)) + params.push((GoIdentifier::local(name), *typ)) } - typ => params.push((GoIdentifier::Local { name }, typ)), + typ => params.push((GoIdentifier::local(name), typ)), } } @@ -2065,7 +1743,7 @@ fn main() -> Result { .map(|(arg, (param, _))| (arg, param)) .collect::>(); - let fn_name = &GoIdentifier::Public { name: &func.name }; + let fn_name = &GoIdentifier::public(&func.name); // TODO(#16): Don't use the internal bindings.out field quote_in! { bindings.out => $['\n'] From bc62c88e9697bb857a1bd0db672d00e522c7e59e Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Fri, 3 Oct 2025 15:50:07 +0100 Subject: [PATCH 2/2] Remove unused weird lifetime on `GoIdentifier::local` --- cmd/gravity/src/go/identifier.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/gravity/src/go/identifier.rs b/cmd/gravity/src/go/identifier.rs index 4e0c7a2..dec7bc9 100644 --- a/cmd/gravity/src/go/identifier.rs +++ b/cmd/gravity/src/go/identifier.rs @@ -30,7 +30,7 @@ impl<'a> GoIdentifier<'a> { } /// Creates a new local identifier. - pub fn local<'b: 'a>(name: &'a str) -> Self { + pub fn local(name: &'a str) -> Self { Self::Local { name } }