stellar/xdrgen

View on GitHub
lib/xdrgen/generators/go.rb

Summary

Maintainability
F
1 wk
Test Coverage
module Xdrgen
  module Generators

    class Go < Xdrgen::Generators::Base

      def generate
        @already_rendered = []
        path = "#{@namespace}_generated.go"
        out = @output.open(path)

        render_top_matter out
        render_definitions(out, @top)
        render_bottom_matter out
      end

      private

      def render_typedef(out, typedef)
        # Typedefs that wrap a pointer type are not well supported in Go because
        # Go does not allow pointer types to have methods. This prevents us from
        # defining the EncodeTo method on these types which is very inconvenient
        # for the render functions that generate structs that contain these
        # types, because xdrgen doesn't know in that moment they are a type
        # without EncodeTo. Since this type cannot have its own methods, we make
        # it a type alias so at least it inherits the EncodeTo method from the
        # aliased type. This is a bit of a hack, and the hack will only work as
        # long as the aliased type is another defined type that has an EncodeTo.
        if typedef.sub_type == :optional
          out.puts "type #{name typedef} = #{reference typedef.declaration.type}"
        else
          out.puts "type #{name typedef} #{reference typedef.declaration.type}"
        end

        # write sizing restrictions
        case typedef.declaration
        when Xdrgen::AST::Declarations::String
          render_maxsize_method out, typedef, typedef.declaration.resolved_size
        when Xdrgen::AST::Declarations::Opaque
          render_maxsize_method out, typedef, typedef.declaration.resolved_size
        when Xdrgen::AST::Declarations::Array
          unless typedef.declaration.fixed?
            render_maxsize_method out, typedef, typedef.declaration.resolved_size
          end
        end

        return unless typedef.sub_type == :simple

        resolved = typedef.resolved_type

        case resolved
        when AST::Definitions::Enum
          render_enum_typedef out, typedef, resolved
        when AST::Definitions::Union
          render_union_typedef out, typedef, resolved
        end

        out.break
      end

      def render_maxsize_method(out, typedef, size)
        return if size.blank?

        out.puts <<-EOS.strip_heredoc
          // XDRMaxSize implements the Sized interface for #{name typedef}
          func (e #{name typedef}) XDRMaxSize() int {
            return #{size}
          }
        EOS
      end

      def render_enum_typedef(out, typedef, enum)
        out.puts <<-EOS.strip_heredoc
          // ValidEnum validates a proposed value for this enum.  Implements
          // the Enum interface for #{name typedef}
          func (e #{name typedef}) ValidEnum(v int32) bool {
            return #{name enum}(e).ValidEnum(v)
          }
        EOS

        out.puts <<-EOS.strip_heredoc
          // String returns the name of `e`
          func (e #{name typedef}) String() string {
            return #{name enum}(e).String()
          }
        EOS

        out.break
      end

      def render_union_typedef(out, typedef, union)
        out.puts <<-EOS.strip_heredoc
          // SwitchFieldName returns the field name in which this union's
          // discriminant is stored
          func (u #{name typedef}) SwitchFieldName() string {
            return #{name union}(u).SwitchFieldName()
          }
        EOS

        out.break

        out.puts <<-EOS.strip_heredoc
          // ArmForSwitch returns which field name should be used for storing
          // the value for an instance of #{name union}
          func (u #{name typedef}) ArmForSwitch(sw int32) (string, bool) {
            return #{name union}(u).ArmForSwitch(sw)
          }
        EOS

        out.break

        constructor_name  = "New#{name typedef}"
        discriminant_arg = private_name union.discriminant
        discriminant_type = reference union.discriminant.type

        out.puts <<-EOS.strip_heredoc
          // #{constructor_name} creates a new  #{name typedef}.
          func #{constructor_name}(#{discriminant_arg} #{discriminant_type}, value interface{}) (result #{reference typedef}, err error) {
            u, err := New#{name union}(#{discriminant_arg}, value)
            result = #{name typedef}(u)
            return
          }
        EOS

        out.break

        # Add accessors for of form val, ok := union.GetArmName()
        # and val := union.MustArmName()
        union.arms.each do |arm|
          next if arm.void?
          out.puts   <<-EOS.strip_heredoc
            // Must#{name arm} retrieves the #{name arm} value from the union,
            // panicing if the value is not set.
            func (u #{name typedef}) Must#{name arm}() #{reference arm.type} {
              return #{name union}(u).Must#{name arm}()
            }

            // Get#{name arm} retrieves the #{name arm} value from the union,
            // returning ok if the union's switch indicated the value is valid.
            func (u #{name typedef}) Get#{name arm}() (result #{reference arm.type}, ok bool) {
              return #{name union}(u).Get#{name arm}()
            }
          EOS
        end

      end

      def render_const(out, const)
        out.puts "const #{name const} = #{const.value}"
        out.break
      end

      def render_definitions(out, node)
        node.definitions.each{|n| render_definition out, n }
        node.namespaces.each{|n| render_definitions out, n }
      end

      def render_nested_definitions(out, defn)
        return unless defn.respond_to? :nested_definitions
        defn.nested_definitions.each{|ndefn| render_definition out, ndefn}
      end

      def render_definition(out, defn)
        if @already_rendered.include? name(defn)

          unless defn.is_a?(AST::Definitions::Namespace)
            $stderr.puts "warn: #{name(defn)} is defined twice.  skipping"
          end

          return
        end

        render_nested_definitions(out, defn)
        render_source_comment(out, defn)

        @already_rendered << name(defn)

        case defn
        when AST::Definitions::Struct ;
          render_struct out, defn
          render_struct_encode_to_interface out, defn
          render_decoder_from_interface out, name(defn)
          render_struct_decode_from_interface out, defn
          render_binary_interface out, name(defn)
          render_xdr_type_interface out, name(defn)
        when AST::Definitions::Enum ;
          render_enum out, defn
          render_enum_encode_to_interface out, defn
          render_decoder_from_interface out, name(defn)
          render_enum_decode_from_interface out, defn
          render_binary_interface out, name(defn)
          render_xdr_type_interface out, name(defn)
        when AST::Definitions::Union ;
          render_union out, defn
          render_union_encode_to_interface out, defn
          render_decoder_from_interface out, name(defn)
          render_union_decode_from_interface out, defn
          render_binary_interface out, name(defn)
          render_xdr_type_interface out, name(defn)
        when AST::Definitions::Typedef ;
          render_typedef out, defn
          # Typedefs that wrap a pointer type are not supported in Go because Go
          # does not allow pointer types to have methods. Don't define methods
          # for the type because that will be a Go compiler error.
          if defn.sub_type != :optional
            render_typedef_encode_to_interface out, defn
            render_decoder_from_interface out, name(defn)
            render_typedef_decode_from_interface out, defn
            render_binary_interface out, name(defn)
            render_xdr_type_interface out, name(defn)
          end
        when AST::Definitions::Const ;
          render_const out, defn
        end
      end

      def render_source_comment(out, defn)
        return if defn.is_a?(AST::Definitions::Namespace)

        out.puts <<-EOS.strip_heredoc
          // #{name defn} is an XDR #{defn.class.name.demodulize} defines as:
          //
        EOS

        out.puts "//   " + defn.text_value.split("\n").join("\n//    ")

        out.puts <<-EOS.strip_heredoc
          //
        EOS
      end

      def render_struct(out, struct)
        out.puts "type #{name struct} struct {"
        out.indent do

          struct.members.each do |m|
            out.puts "#{name m} #{reference(m.declaration.type)} #{field_tag struct, m}"
          end

        end
        out.puts "}"
        out.break
      end

      def render_enum(out, enum)
        # render the "enum"
        out.puts "type #{name enum} int32"
        out.puts "const ("
        out.indent do
          first_member = enum.members.first
          out.puts "#{name enum}#{name first_member} #{name enum} = #{first_member.value}"

          rest_members = enum.members.drop(1)
          rest_members.each do |m|
            out.puts "#{name enum}#{name m} #{name enum} = #{m.value}"
          end
        end
        out.puts ")"

        # render the map used by xdr to decide valid values
        out.puts "var #{private_name enum}Map = map[int32]string{"
        out.indent do

          enum.members.each do |m|
            out.puts "#{m.value}: \"#{name enum}#{name m}\","
          end

        end
        out.puts "}"

        out.break

        out.puts <<-EOS.strip_heredoc
          // ValidEnum validates a proposed value for this enum.  Implements
          // the Enum interface for #{name enum}
          func (e #{name enum}) ValidEnum(v int32) bool {
            _, ok := #{private_name enum}Map[v]
            return ok
          }
        EOS

        out.puts <<-EOS.strip_heredoc
          // String returns the name of `e`
          func (e #{name enum}) String() string {
            name, _ := #{private_name enum}Map[int32(e)]
            return name
          }
        EOS

        out.break
      end

      def render_union(out, union)

        out.puts "type #{name union} struct{"
        out.indent do
          out.puts "#{name union.discriminant} #{reference union.discriminant.type}"

          union.arms.each do |arm|
            next if arm.void?
            out.puts "#{name arm} *#{reference arm.type} #{field_tag union, arm}"
          end
        end
        out.puts "}"
        out.break

        out.puts <<-EOS.strip_heredoc
          // SwitchFieldName returns the field name in which this union's
          // discriminant is stored
          func (u #{name union}) SwitchFieldName() string {
            return "#{name union.discriminant}"
          }
        EOS

        out.break

        out.puts <<-EOS.strip_heredoc
          // ArmForSwitch returns which field name should be used for storing
          // the value for an instance of #{name union}
          func (u #{name union}) ArmForSwitch(sw int32) (string, bool) {
        EOS

        switch_for(out, union, "sw") do |arm, kase|
          "return \"#{name arm unless arm.void?}\", true"
        end

        # when the default arm is not present, we must render the failure case
        unless union.default_arm.present?
          out.puts 'return "-", false'
        end

        out.puts "}"
        out.break

        # Add constructor of the form u := NewUnion(switch,val)
        render_union_constructor(out, union)

        # Add accessors for of form val, ok := union.GetArmName()
        # and val := union.MustArmName()
        union.arms.each do |arm|
          next if arm.void?
          out.puts access_arm(arm)
        end

        out.break
      end

      def render_struct_encode_to_interface(out, struct)
        name = name(struct)
        out.puts "// EncodeTo encodes this value using the Encoder."
        out.puts "func (s *#{name}) EncodeTo(e *xdr.Encoder) error {"
        out.puts "  var err error"
        struct.members.each do |m|
          mn = name(m)
          render_encode_to_body(out, "s.#{mn}", m.type, self_encode: false)
        end
        out.puts "  return nil"
        out.puts "}"
        out.break
      end

      def render_union_encode_to_interface(out, union)
        name = name(union)
        out.puts "// EncodeTo encodes this value using the Encoder."
        out.puts "func (u #{name}) EncodeTo(e *xdr.Encoder) error {"
        out.puts "  var err error"
        render_encode_to_body(out, "u.#{name(union.discriminant)}", union.discriminant.type, self_encode: false)
        switch_for(out, union, "u.#{name(union.discriminant)}") do |arm, kase|
          out2 = StringIO.new
          if arm.void?
            out2.puts "// Void"
          else
            mn = name(arm)
            render_encode_to_body(out2, "(*u.#{mn})", arm.type, self_encode: false)
          end
          out2.puts "return nil"
          out2.string
        end

        # when the default arm is not present, we must render the failure case
        unless union.default_arm.present?
          out.puts "  return fmt.Errorf(\"#{name(union.discriminant)} (#{reference union.discriminant.type}) switch value '%d' is not valid for union #{name}\", u.#{name(union.discriminant)})"
        end

        out.puts "}"
        out.break
      end

      def render_enum_encode_to_interface(out, typedef)
        name = name(typedef)
        type = typedef
        out.puts <<-EOS.strip_heredoc
        // EncodeTo encodes this value using the Encoder.
        func (e #{name}) EncodeTo(enc *xdr.Encoder) error {
          if _, ok := #{private_name type}Map[int32(e)]; !ok {
            return fmt.Errorf("'%d' is not a valid #{name} enum value", e)
          }
          _, err := enc.EncodeInt(int32(e))
          return err
        }
        EOS
      end

      def is_fixed_array_type(type)
        (type.is_a?(AST::Typespecs::Opaque) && type.fixed?) || type.sub_type == :array
      end

      def render_typedef_encode_to_interface(out, typedef)
        name = name(typedef)
        type = typedef.declaration.type
        out.puts "// EncodeTo encodes this value using the Encoder."
        if is_fixed_array_type(type) ||
            (type.is_a?(AST::Identifier) && type.sub_type == :simple && type.resolved_type.is_a?(AST::Definitions::Typedef) && is_fixed_array_type(type.resolved_type.declaration.type))
          # Implement EncodeTo by pointer for Go array types
          # otherwise (if called by value), Go will make a heap allocation
          # for every by-value call since the copy required by the call
          # tends to escape the stack due to the large array sizes.
          out.puts "func (s *#{name}) EncodeTo(e *xdr.Encoder) error {"
        else
          out.puts "func (s #{name}) EncodeTo(e *xdr.Encoder) error {"
        end
        out.puts "  var err error"
        render_encode_to_body(out, "s", type, self_encode: true)
        out.puts "  return nil"
        out.puts "}"
        out.break
      end

      # render_encode_to_body assumes there is an `e` variable containing an
      # xdr.Encoder, and a variable defined by `var` that is the value to
      # encode.
      def render_encode_to_body(out, var, type, self_encode:)
        def check_error(str)
          <<-EOS
  if #{str}; err != nil {
    return err
  }
EOS
        end
        optional = type.sub_type == :optional
        if optional
          out.puts check_error "_, err = e.EncodeBool(#{var} != nil)"
          out.puts "  if #{var} != nil {"
          var = "(*#{var})"
        end
        case type
        when AST::Typespecs::UnsignedHyper
          out.puts check_error "  _, err = e.EncodeUhyper(uint64(#{var}))"
        when AST::Typespecs::Hyper
          out.puts check_error "_, err = e.EncodeHyper(int64(#{var}))"
        when AST::Typespecs::UnsignedInt
          out.puts check_error "_, err = e.EncodeUint(uint32(#{var}))"
        when AST::Typespecs::Int
          out.puts (check_error "_, err = e.EncodeInt(int32(#{var}))")
        when AST::Typespecs::Bool
          out.puts (check_error "_, err = e.EncodeBool(bool(#{var}))")
        when AST::Typespecs::String
          out.puts check_error "_, err = e.EncodeString(string(#{var}))"
        when AST::Typespecs::Opaque
          if type.fixed?
            out.puts check_error "_, err = e.EncodeFixedOpaque(#{var}[:])"
          else
            out.puts check_error "_, err = e.EncodeOpaque(#{var}[:])"
          end
        when AST::Typespecs::Simple
          case type.sub_type
          when :simple, :optional
            optional_within = type.is_a?(AST::Identifier) && type.resolved_type.sub_type == :optional
            if optional_within
              out.puts check_error "_, err = e.EncodeBool(#{var} != nil)"
              out.puts "  if #{var} != nil {"
              var = "(*#{var})"
            end
            if self_encode
              newvar = "#{name type}(#{var})"
              if type.resolved_type.is_a?(AST::Definitions::Typedef) && is_fixed_array_type(type.resolved_type.declaration.type)
                # Go array types implement EncodeTo by pointer
                if type.is_a?(AST::Identifier)
                  # we are already calling by pointer, so we just need to cast
                  newvar = "(*#{name type})(#{var})"
                else
                  newvar = "(*#{name type})(&#{var})"
                end
              end
              var = newvar
            end
            out.puts check_error "  err = #{var}.EncodeTo(e)"
            if optional_within
              out.puts "  }"
            end
          when :array
            out.puts "  for i := 0; i < len(#{var}); i++ {"
            element_var = "#{var}[i]"
            optional_within = type.is_a?(AST::Identifier) && type.resolved_type.sub_type == :optional
            if optional_within
              out.puts check_error "_, err = e.EncodeBool(#{element_var} != nil)"
              out.puts "    if #{element_var} != nil {"
              var = "(*#{element_var})"
            end
            out.puts check_error "err = #{element_var}.EncodeTo(e)"
            if optional_within
              out.puts "    }"
            end
            out.puts "  }"
          when :var_array
            out.puts check_error "_, err = e.EncodeUint(uint32(len(#{var})))"
            out.puts "  for i := 0; i < len(#{var}); i++ {"
            element_var = "#{var}[i]"
            optional_within = type.is_a?(AST::Identifier) && type.resolved_type.sub_type == :optional
            if optional_within
              out.puts check_error "_, err = e.EncodeBool(#{element_var} != nil)"
              out.puts "    if #{element_var} != nil {"
              var = "(*#{element_var})"
            end
            out.puts check_error "err = #{element_var}.EncodeTo(e)"
            if optional_within
              out.puts "    }"
            end
            out.puts "  }"
          else
            raise "Unknown sub_type: #{type.sub_type}"
          end
        when AST::Definitions::Base
          if self_encode
            out.puts check_error "err = #{name type}(#{var}).EncodeTo(e)"
          else
            out.puts check_error "err = #{var}.EncodeTo(e)"
          end
        else
          out.puts check_error "_, err = e.Encode(#{var})"
        end
        if optional
          out.puts "  }"
        end
      end

      def render_struct_decode_from_interface(out, struct)
        name = name(struct)
        out.puts "// DecodeFrom decodes this value using the Decoder."
        out.puts "func (s *#{name}) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) {"
        out.puts "  if maxDepth == 0 {"
        out.puts "    return 0, fmt.Errorf(\"decoding #{name}: %w\", ErrMaxDecodingDepthReached)"
        out.puts "  }"
        out.puts "  maxDepth -= 1"
        out.puts "  var err error"
        out.puts "  var n, nTmp int"
        declared_variables = []
        struct.members.each do |m|
          mn = name(m)
          render_decode_from_body(out, "s.#{mn}", m.type, declared_variables: declared_variables, self_encode: false)
        end
        out.puts "  return n, nil"
        out.puts "}"
        out.break
      end

      def render_union_decode_from_interface(out, union)
        name = name(union)
        out.puts "// DecodeFrom decodes this value using the Decoder."
        out.puts "func (u *#{name}) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) {"
        out.puts "  if maxDepth == 0 {"
        out.puts "    return 0, fmt.Errorf(\"decoding #{name}: %w\", ErrMaxDecodingDepthReached)"
        out.puts "  }"
        out.puts "  maxDepth -= 1"
        out.puts "  var err error"
        out.puts "  var n, nTmp int"
        render_decode_from_body(out, "u.#{name(union.discriminant)}", union.discriminant.type, declared_variables: [], self_encode: false)
        switch_for(out, union, "u.#{name(union.discriminant)}") do |arm, kase|
          out2 = StringIO.new
          if arm.void?
            out2.puts "// Void"
          else
            mn = name(arm)
            type = arm.type
            out2.puts "  u.#{mn} = new(#{reference arm.type})"
            render_decode_from_body(out2, "(*u.#{mn})",type, declared_variables: [], self_encode: false)
          end
          out2.puts "  return n, nil"
          out2.string
        end
        unless union.default_arm.present?
                    out.puts "  return n, fmt.Errorf(\"union #{name} has invalid #{name(union.discriminant)} (#{reference union.discriminant.type}) switch value '%d'\", u.#{name(union.discriminant)})"
        end
        out.puts "}"
        out.break
      end

      def render_enum_decode_from_interface(out, typedef)
        name = name(typedef)
        type = typedef
        out.puts <<-EOS.strip_heredoc
        // DecodeFrom decodes this value using the Decoder.
        func (e *#{name}) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) {
          if maxDepth == 0 {
            return 0, fmt.Errorf("decoding #{name}: %w", ErrMaxDecodingDepthReached)
          }
          maxDepth -= 1
          v, n, err := d.DecodeInt()
          if err != nil {
            return n, fmt.Errorf("decoding #{name}: %w", err)
          }
          if _, ok := #{private_name type}Map[v]; !ok {
            return n, fmt.Errorf("'%d' is not a valid #{name} enum value", v)
          }
          *e = #{name}(v)
          return n, nil
        }
        EOS
      end

      def render_typedef_decode_from_interface(out, typedef)
        name = name(typedef)
        type = typedef.declaration.type
        out.puts "// DecodeFrom decodes this value using the Decoder."
        out.puts "func (s *#{name}) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) {"
        out.puts "  if maxDepth == 0 {"
        out.puts "    return 0, fmt.Errorf(\"decoding #{name}: %w\", ErrMaxDecodingDepthReached)"
        out.puts "  }"
        out.puts "  maxDepth -= 1"
        out.puts "  var err error"
        out.puts "  var n, nTmp int"
        var = "s"
        sub_var_type = ""
        case type
        when AST::Typespecs::UnsignedHyper, AST::Typespecs::Hyper, AST::Typespecs::UnsignedInt, AST::Typespecs::Int, AST::Typespecs::String
          sub_var_type = reference(type)
        end
        if (type.is_a?(AST::Typespecs::Opaque) && !type.fixed?) || (type.is_a?(AST::Typespecs::Simple) && type.sub_type == :var_array)
            var = "(*s)"
        end
        unless sub_var_type.empty?
          out.puts "  var v #{sub_var_type}"
          var = "v"
        end
        render_decode_from_body(out, var, type, declared_variables: [], self_encode: true)
        out.puts "  *s = #{name}(v)" unless sub_var_type.empty?
        out.puts "  return n, nil"
        out.puts "}"
        out.break
      end

      def render_variable_declaration(out, indent, var, type, declared_variables:)
        unless declared_variables.include?var
           out.puts "#{indent}var #{var} #{type}"
           declared_variables.append(var)
        end
      end

      # render_decode_from_body assumes there is an `d` variable containing an
      # xdr.Decoder, and a variable defined by `var` that is the value to
      # encode.
      def render_decode_from_body(out, var, type, declared_variables:, self_encode:)
        tail = <<-EOS
  n += nTmp
  if err != nil {
    return n, fmt.Errorf("decoding #{name type}: %w", err)
  }
EOS
        optional = type.sub_type == :optional
        if optional
          render_variable_declaration(out, "  ", 'b', "bool", declared_variables: declared_variables)
          out.puts "  b, nTmp, err = d.DecodeBool()"
          out.puts tail
          out.puts "  #{var} = nil"
          out.puts "  if b {"
          out.puts "     #{var} = new(#{name type})"
        end
        case type
        when AST::Typespecs::UnsignedHyper
          out.puts "  #{var}, nTmp, err = d.DecodeUhyper()"
          out.puts tail
        when AST::Typespecs::Hyper
          out.puts "  #{var}, nTmp, err = d.DecodeHyper()"
          out.puts tail
        when AST::Typespecs::UnsignedInt
          out.puts "  #{var}, nTmp, err = d.DecodeUint()"
          out.puts tail
        when AST::Typespecs::Int
          out.puts "  #{var}, nTmp, err = d.DecodeInt()"
          out.puts tail
        when AST::Typespecs::Bool
          out.puts "  #{var}, nTmp, err = d.DecodeBool()"
          out.puts tail
        when AST::Typespecs::String
          arg = "0"
          arg = type.decl.resolved_size unless type.decl.resolved_size.nil?
          out.puts "  #{var}, nTmp, err = d.DecodeString(#{arg})"
          out.puts tail
        when AST::Typespecs::Opaque
          if type.fixed?
            out.puts "  nTmp, err = d.DecodeFixedOpaqueInplace(#{var}[:])"
          else
            arg = "0"
            arg = type.decl.resolved_size unless type.decl.resolved_size.nil?
            out.puts "  #{var}, nTmp, err = d.DecodeOpaque(#{arg})"
          end
          out.puts tail
        when AST::Typespecs::Simple
          case type.sub_type
          when :simple, :optional
            optional_within = type.is_a?(AST::Identifier) && type.resolved_type.sub_type == :optional
            if optional_within
              render_variable_declaration(out, "  ", 'b', "bool", declared_variables: declared_variables)
              out.puts "  b, nTmp, err = d.DecodeBool()"
              out.puts tail
              out.puts "  #{var} = nil"
              out.puts "  if b {"
              out.puts "     #{var} = new(#{name type.resolved_type.declaration.type})"
            end
            var = "(*#{name type})(#{var})" if self_encode
            out.puts "  nTmp, err = #{var}.DecodeFrom(d, maxDepth)"
            out.puts tail
            if optional_within
              out.puts "  }"
            end
          when :array
            out.puts "  for i := 0; i < len(#{var}); i++ {"
            element_var = "#{var}[i]"
            optional_within = type.is_a?(AST::Identifier) && type.resolved_type.sub_type == :optional
            if optional_within
              out.puts "    var eb bool"
              out.puts "    eb, nTmp, err = d.DecodeBool()"
              out.puts tail
              out.puts "    #{var} = nil"
              out.puts "    if eb {"
              var = "(*#{element_var})"
            end
            out.puts "      nTmp, err = #{element_var}.DecodeFrom(d, maxDepth)"
            out.puts tail
            if optional_within
              out.puts "    }"
            end
            out.puts "  }"
          when :var_array
            render_variable_declaration(out, "  ", 'l', "uint32", declared_variables: declared_variables)
            out.puts "  l, nTmp, err = d.DecodeUint()"
            out.puts tail
            unless type.decl.resolved_size.nil?
               out.puts "  if l > #{type.decl.resolved_size} {"
               out.puts "    return n, fmt.Errorf(\"decoding #{name type}: data size (%d) exceeds size limit (#{type.decl.resolved_size})\", l)"
               out.puts "  }"
            end
            out.puts "  #{var} = nil"
            out.puts "  if l > 0 {"
            out.puts "    if il, ok := d.InputLen(); ok && uint(il) < uint(l) {"
            out.puts "        return n, fmt.Errorf(\"decoding #{name type}: length (%d) exceeds remaining input length (%d)\", l, il)"
            out.puts "    }"
            out.puts "    #{var} = make([]#{name type}, l)"
            out.puts "    for i := uint32(0); i < l; i++ {"
            element_var =   "#{var}[i]"
            optional_within = type.is_a?(AST::Identifier) && type.resolved_type.sub_type == :optional
            if optional_within
              out.puts "      var eb bool"
              out.puts "      eb, nTmp,  err = d.DecodeBool()"
              out.puts tail
              out.puts "      #{element_var} = nil"
              out.puts "      if eb {"
              out.puts "         #{element_var} = new(#{name type.resolved_type.declaration.type})"
              var = "(*#{element_var})"
            end
            out.puts "      nTmp, err = #{element_var}.DecodeFrom(d, maxDepth)"
            out.puts tail
            if optional_within
              out.puts "    }"
            end
            out.puts "    }"
            out.puts "  }"
          else
            raise "Unknown sub_type: #{type.sub_type}"
          end
        when AST::Definitions::Base
          if self_encode
            out.puts "  nTmp, err = #{name type}(#{var}).DecodeFrom(d, maxDepth)"
          else
            out.puts "  nTmp, err = #{var}.DecodeFrom(d, maxDepth)"
          end
          out.puts tail
        else
          out.puts "  nTmp, err = d.DecodeWithMaxDepth(&#{var}, maxDepth)"
          out.puts tail
        end
        if optional
           out.puts "  }"
        end
      end

      def render_binary_interface(out, name)
        out.puts "// MarshalBinary implements encoding.BinaryMarshaler."
        out.puts "func (s #{name}) MarshalBinary() ([]byte, error) {"
        out.puts "  b := bytes.Buffer{}"
        out.puts "  e := xdr.NewEncoder(&b)"
        out.puts "  err := s.EncodeTo(e)"
        out.puts "  return b.Bytes(), err"
        out.puts "}"
        out.break
        out.puts "// UnmarshalBinary implements encoding.BinaryUnmarshaler."
        out.puts "func (s *#{name}) UnmarshalBinary(inp []byte) error {"
        out.puts "  r := bytes.NewReader(inp)"
        out.puts "  o := xdr.DefaultDecodeOptions"
        out.puts "  o.MaxInputLen = len(inp)"
        out.puts "  d := xdr.NewDecoderWithOptions(r, o)"
        out.puts "  _, err := s.DecodeFrom(d, o.MaxDepth)"
        out.puts "  return err"
        out.puts "}"
        out.break
        out.puts "var ("
        out.puts "  _ encoding.BinaryMarshaler   = (*#{name})(nil)"
        out.puts "  _ encoding.BinaryUnmarshaler = (*#{name})(nil)"
        out.puts ")"
        out.break
      end

      def render_xdr_type_interface(out, name)
        out.puts "// xdrType signals that this type represents XDR values defined by this package."
        out.puts "func (s #{name}) xdrType() {}"
        out.break
        out.puts "var _ xdrType = (*#{name})(nil)"
        out.break
      end

      def render_decoder_from_interface(out, name)
        out.puts "var _ decoderFrom = (*#{name})(nil)"
      end

      def render_top_matter(out)
        out.puts <<-EOS.strip_heredoc
          //lint:file-ignore S1005 The issue should be fixed in xdrgen. Unfortunately, there's no way to ignore a single file in staticcheck.
          //lint:file-ignore U1000 fmtTest is not needed anywhere, should be removed in xdrgen.

          // Package #{@namespace || "main"} is generated from:
          //
          //  #{@output.relative_source_paths.join("\n//  ")}
          //
          // DO NOT EDIT or your changes may be overwritten
          package #{@namespace || "main"}

          import (
            "bytes"
            "encoding"
            "errors"
            "io"
            "fmt"

            "github.com/stellar/go-xdr/xdr3"
          )
        EOS
        out.break
        out.puts <<-EOS.strip_heredoc
        // XdrFilesSHA256 is the SHA256 hashes of source files.
        var XdrFilesSHA256 = map[string]string{
          #{@output.relative_source_path_sha256_hashes.map(){ |path, hash| %{"#{path}": "#{hash}",} }.join("\n")}
        }
        EOS
        out.break
        out.puts <<-EOS.strip_heredoc
          var ErrMaxDecodingDepthReached = errors.New("maximum decoding depth reached")

          type xdrType interface {
            xdrType()
          }

          type decoderFrom interface {
            DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error)
          }

          // Unmarshal reads an xdr element from `r` into `v`.
          func Unmarshal(r io.Reader, v interface{}) (int, error) {
            return UnmarshalWithOptions(r, v, xdr.DefaultDecodeOptions)
          }

          // UnmarshalWithOptions works like Unmarshal but uses decoding options.
          func UnmarshalWithOptions(r io.Reader, v interface{}, options xdr.DecodeOptions) (int, error) {
            if decodable, ok := v.(decoderFrom); ok {
              d := xdr.NewDecoderWithOptions(r, options)
              return decodable.DecodeFrom(d, options.MaxDepth)
            }
            // delegate to xdr package's Unmarshal
              return xdr.UnmarshalWithOptions(r, v, options)
          }

          // Marshal writes an xdr element `v` into `w`.
          func Marshal(w io.Writer, v interface{}) (int, error) {
            if _, ok := v.(xdrType); ok {
              if bm, ok := v.(encoding.BinaryMarshaler); ok {
                b, err := bm.MarshalBinary()
                if err != nil {
                  return 0, err
                }
                return w.Write(b)
              }
            }
            // delegate to xdr package's Marshal
            return xdr.Marshal(w, v)
          }
        EOS
        out.break
      end

      def render_bottom_matter(out)
        out.puts 'var fmtTest = fmt.Sprint("this is a dummy usage of fmt")'
      end

      private

      def reference(type)
        baseReference = case type
        when AST::Typespecs::Bool
          "bool"
        when AST::Typespecs::Double
          "float64"
        when AST::Typespecs::Float
          "float32"
        when AST::Typespecs::Hyper
          "int64"
        when AST::Typespecs::Int
          "int32"
        when AST::Typespecs::Opaque
          if type.fixed?
            "[#{type.size}]byte"
          else
            "[]byte"
          end
        when AST::Typespecs::Quadruple
          raise "no quadruple support for go"
        when AST::Typespecs::String
          "string"
        when AST::Typespecs::UnsignedHyper
          "uint64"
        when AST::Typespecs::UnsignedInt
          "uint32"
        when AST::Typespecs::Simple
          name type
        when AST::Definitions::Base
          name type
        when AST::Concerns::NestedDefinition
          name type
        else
          raise "Unknown reference type: #{type.class.name}, #{type.class.ancestors}"
        end

        case type.sub_type
        when :simple
          baseReference
        when :optional
          "*#{baseReference}"
        when :array
          is_named, size = type.array_size

          # if named, lookup the const definition
          if is_named
            size = name @top.find_definition(size)
          end

          "[#{size}]#{baseReference}"
        when :var_array
          "[#{size}]#{baseReference}"
        else
          raise "Unknown sub_type: #{type.sub_type}"
        end

      end

      def name(named)
        parent = name named.parent_defn if named.is_a?(AST::Concerns::NestedDefinition)

        base = if named.respond_to?(:name)
          named.name
        else
          named.text_value
        end

        "#{parent}#{base.underscore.camelize}"
      end

      def private_name(named)
        escape_name named.name.underscore.camelize(:lower)
      end

      def escape_name(name)
        case name
        when "type" ; "aType"
        when "func" ; "aFunc"
        else ; name
        end
      end

      def render_union_constructor(out, union)
        constructor_name  = "New#{name union}"


        discriminant_arg = private_name union.discriminant
        discriminant_type = reference union.discriminant.type

        out.puts <<-EOS.strip_heredoc
          // #{constructor_name} creates a new  #{name union}.
          func #{constructor_name}(#{discriminant_arg} #{discriminant_type}, value interface{}) (result #{reference union}, err error) {
            result.#{name union.discriminant} = #{discriminant_arg}
        EOS

        switch_for(out, union, discriminant_arg) do |arm, kase|
          if arm.void?
            "// void"
          else
            <<-EOS
            tv, ok := value.(#{reference arm.type})
            if !ok {
              err = errors.New("invalid value, must be #{reference arm.type}")
              return
            }
            result.#{name arm} = &tv
            EOS
          end
        end

        out.puts <<-EOS.strip_heredoc
            return
          }
        EOS
      end

      def access_arm(arm)

        <<-EOS.strip_heredoc
          // Must#{name arm} retrieves the #{name arm} value from the union,
          // panicing if the value is not set.
          func (u #{name arm.union}) Must#{name arm}() #{reference arm.type} {
            val, ok := u.Get#{name arm}()

            if !ok {
              panic("arm #{name arm} is not set")
            }

            return val
          }

          // Get#{name arm} retrieves the #{name arm} value from the union,
          // returning ok if the union's switch indicated the value is valid.
          func (u #{name arm.union}) Get#{name arm}() (result #{reference arm.type}, ok bool) {
            armName, _ := u.ArmForSwitch(int32(u.#{name arm.union.discriminant}))

            if armName == "#{name arm}" {
              result = *u.#{name arm}
              ok = true
            }

            return
          }
        EOS
      end

      def size(size_s)
        result = size_s
        result = "MaxXdrElements" if result.blank?
        result
      end

      def switch_for(out, union, ident)
        out.puts "switch #{reference union.discriminant.type}(#{ident}) {"

        union.normal_arms.each do |arm|
          arm.cases.each do |c|

            value = if c.value.is_a?(AST::Identifier)
                      member = union.resolved_case(c)
                      if union.discriminant_type.nil? then
                        "int32(#{name member.enum}#{name member})"
                      else
                        "#{name union.discriminant_type}#{name member}"
                      end
                    else
                      c.value.text_value
                    end

            out.puts "    case #{value}:"
            out.puts "      #{yield arm, c}"
          end
        end

        if union.default_arm.present?
          arm = union.default_arm
          out.puts "    default:"
          out.puts "      #{yield arm, :default}"
        end

        out.puts "}"
      end

      def field_tag(struct, field)
        size = nil

        case field.declaration
        when Xdrgen::AST::Declarations::Opaque
          size = field.declaration.resolved_size
        when Xdrgen::AST::Declarations::String
          size = field.declaration.resolved_size
        when Xdrgen::AST::Declarations::Array
          size = field.declaration.resolved_size unless field.declaration.fixed?
        end

        return "`xdrmaxsize:\"#{size}\"`" if size.present?
      end

    end

  end
end