#!/usr/bin/env ruby
# Copyright (c) 2023, gperftools Contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

require 'digest'

# This is main logic. If you want to add new ucontext-to-pc accessor, add it here
def dwm!(d)
  d.template "NetBSD has really nice portable macros", :_UC_MACHINE_PC do |uc|
    "_UC_MACHINE_PC(#{uc})"
  end

  d.with_prefix "uc_mcontext." do
    # first arg is ifdef, second is field (with prefix prepended) and third is comment
    d.ifdef :REG_PC, "gregs[REG_PC]", "Solaris/x86"
    d.ifdef :REG_EIP, "gregs[REG_EIP]", "Linux/i386"
    d.ifdef :REG_RIP, "gregs[REG_RIP]", "Linux/amd64"
    d.field "sc_ip", "Linux/ia64"
    d.field "__pc", "Linux/loongarch64"
    d.field "pc", "Linux/{mips,aarch64}"
    d.ifdef :PT_NIP, "uc_regs->gregs[PT_NIP]", "Linux/ppc"
    d.ifdef :PT_NIP, "gp_regs[PT_NIP]", "Linux/ppc"
    d.ifdef :REG_PC, "__gregs[REG_PC]", "Linux/riscv"
    d.field "psw.addr", "Linux/s390"
    d.field "arm_pc", "Linux/arm (32-bit; legacy)"
    d.field "mc_eip", "FreeBSD/i386"
    d.field "mc_srr0", "FreeBSD/ppc"
    d.field "mc_rip", "FreeBSD/x86_64"
  end

  d.with_prefix "uc_mcontext->" do
    d.field "ss.eip", "OS X (i386, <=10.4)"
    d.field "__ss.__eip", "OS X (i386, >=10.5)"
    d.field "ss.rip", "OS X (x86_64)"
    d.field "__ss.__rip", "OS X (>=10.5 [untested])"
    d.field "ss.srr0", "OS X (ppc, ppc64 [untested])"
    d.field "__ss.__srr0", "OS X (>=10.5 [untested])"
    d.field "__ss.__pc", "OS X (arm64)"
  end

  d.field "sc_eip", "OpenBSD/i386"
  d.field "sc_rip", "OpenBSD/x86_64"
end

# this is generator logic
class Definer
  def initialize
    @prefix = ""
    @accessors = {}
    puts(<<HERE)
// -*- eval: (read-only-mode) -*-
// WARNING: this file is autogenerated.
// Change and run src/gen_getpc.rb if you want to
// update. (And submit both files)

// What this file does? We have several possible ways of fetching PC
// (program counter) of signal's ucontext. We explicitly choose to
// avoid ifdef-ing specific OSes (or even specific versions), to
// increase our chances that stuff simply works. Comments below refer
// to OS/architecture combos for documentation purposes, but what
// works is what is used.

// How it does it? It uses lightweight C++ template magic where
// "wrong" ucontext_t{nullptr}-><field access> combos are
// automagically filtered out (via SFINAE).

// Each known case is represented as a template class. For SFINAE
// reasons we masquerade ucontext_t type behind U template
// parameter. And we also parameterize by parent class. This allows us
// to arrange all template instantiations in a single ordered chain of
// inheritance. See RawUCToPC below.

// Note, we do anticipate that most times exactly one of those access
// methods works. But we're prepared there could be several. In
// particular, according to previous comments Solaris/x86 also has
// REG_RIP defined, but it is somehow wrong. So we're careful about
// preserving specific order. We couldn't handle this "multiplicity"
// aspect in pure C++, so we use code generation.

namespace internal {

struct Empty {
#ifdef DEFINE_TRIVIAL_GET
#define HAVE_TRIVIAL_GET
  // special thing for stacktrace_generic_fp-inl which wants no-op case
  static void* Get(...) {
    return nullptr;
  }
#endif
};
HERE
  end

  def with_prefix(prefix)
    old_prefix = @prefix
    @prefix = @prefix.dup + prefix
    yield
  ensure
    @prefix = old_prefix
  end

  def ifdef define, field, comment
    field field, comment, define
  end

  def template comment, define = nil, &block
    field block, comment, define
  end

  def field field, comment, define = nil
    tmpl = if field.kind_of? Proc
             raise unless @prefix.empty?
             field
           else
             proc do |uc|
               "#{uc}->#{@prefix + field}"
             end
           end
    fingerprint = Digest::MD5.hexdigest(tmpl["%"] + "@" + comment)[0,8]

    maybe_open_ifdef = "\n#ifdef #{define}" if define
    maybe_close_ifdef = "\n#endif  // #{define}" if define

    raise "conflict!" if @accessors.include? fingerprint

    if define
      @accessors[fingerprint] = comment + " (with #ifdef #{define})"
    else
      @accessors[fingerprint] = comment
    end

    puts(<<HERE)

// #{comment}
template <class U, class P, class = void>
struct get_#{fingerprint} : public P {
};#{maybe_open_ifdef}
template <class U, class P>
struct get_#{fingerprint}<U, P, void_t<decltype(#{tmpl["((U*){})"]})>> : public P {
  static void* Get(const U* uc) {
    // #{comment}
    return (void*)(#{tmpl[:uc]});
  }
};#{maybe_close_ifdef}
HERE
  end

  def finalize!
    puts
    puts(<<HERE)
inline void* RawUCToPC(const ucontext_t* uc) {
HERE
    prev = "Empty"
    @accessors.each_pair.reverse_each do |(fingerprint, comment)|
      puts "  // #{comment}"
      puts "  using g_#{fingerprint} = get_#{fingerprint}<ucontext_t, #{prev}>;"
      prev = "g_#{fingerprint}"
    end
    puts(<<HERE)
  return #{prev}::Get(uc);
}
HERE
    puts
    puts("}  // namespace internal")
  end
end

path = File.join(File.dirname(__FILE__), "getpc-inl.h")
STDOUT.reopen(IO.popen(["tee", path], "w"))

Definer.new.tap {|instance| dwm!(instance)}.finalize!
