わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

「続・関数形式マクロは演算子を引数に取れる」の検証プログラム

続・関数形式マクロは演算子を引数に取れる - わさっきの続き.
Rubyを使った生成プログラムです.

#!/usr/bin/env ruby

# opgen.rb

class OperatorGenerator
  def initialize
    @filename_src = "op.c"
    @filename_bin = "op"
    if /mswin(?!ce)|mingw|cygwin|bccwin/i =~ RUBY_PLATFORM
      @filename_bin += ".exe"
    end
    @command_compile = "cc -o #{@filename_bin} #{@filename_src}"
    @ok_a = []
    @qty_op = 6
  end

  def op(num)
    ("%0#{@qty_op}b" % num).gsub(/0/, "-").gsub(/1/, "+")
  end
  private :op

  def rm_bin
    if test(?f, @filename_bin)
      File.unlink(@filename_bin)
    end
  end
  private :rm_bin

  def gen_code(num)
    code = ""

    0.upto(num) do |j|
      code0 = "opp(a, #{op(j)}, b);"
      if @ok_a[j] == false
        code0 = "/* #{code0} */"
      end
      code += "  #{code0}\n"
    end

    src = <<'EOS'
#include <stdio.h>

#define op(x, y, z) ((x) y (z))
#define p2(x, y) printf(#x " = %d, " #y " = %d\n", x, y)
#define opp(x, y, z) p2(x, z),\
    printf(#x " " #y " " #z " = %d\n", op(x, y, z)),\
    p2(x, z), puts("")

int main(void)
{
  int a = 4, b = 2;
__CODE__
  return 0;
}
EOS

    src["__CODE__"] = code

    open(@filename_src, "w") do |g|
      g.print src
      print src if $DEBUG
    end
  end
  private :gen_code

  def p_system(command)
    puts command
    system command
  end
  private :p_system

  def start
    @ok_a = []
    max = 2 ** @qty_op - 1

    0.upto(max) do |i|
      puts "[a #{op(i)} b]"
      gen_code(i)
      rm_bin
      p_system @command_compile
      @ok_a << test(?f, @filename_bin)
    end

    puts "[result]"
    gen_code(max)
    p_system @command_compile
    p_system "./#{@filename_bin}"
  end
end

if __FILE__ == $0
  OperatorGenerator.new.start
end

要所は3か所.

  • 「+と-の並び」の全組み合わせを求めるのに,文字列なり配列なりを作ってそれを変えていくというのは,ループで回しにくいものです.上のコードでは,整数値を,+と-の並びに変換します.方法は,プライベートメソッドopのとおりで,Ruby用sprintfフォーマット*1の%bを使って2進値にし,あとはgsubで0を-に,1を+に置き換えます.
  • 「opp(a, ------, b);」や「opp(a, +-+-+-, b);」といったコードが正しいかを確認するため,一つ一つコンパイルをしています.最初にすべての「+と-の並び」のコードを生成し,コンパイルしてエラー行を見つけるというのは手間と考えたのです.実際には,最終結果のコード生成と共通化させるため,プライベートメソッドgen_codeは引数numをとり,0以上num以下の各整数に対応する「+と-の並び」すべてについて,コードを生成します(すでにエラーになる文はコメントしています).コンパイルして,実行ファイルが作られればOK,作られなければNGです.コンパイルの前に,実行ファイルがもしあれば,消すようにします.
  • どの整数に対応する文はエラーになるかの情報は,配列のインスタンス変数@ok_aに格納させます.@ok_a[整数値]がfalseのときのみ,エラーになることを確認しているという意味です.trueのときはOK,nilのときは未検証ですが,gen_codeの中ではどちらの場合も,コメントとせずにコードを吐いています.

小さいことを2点.

  • 「if /mswin(?!ce)|mingw|cygwin|bccwin/i =~ RUBY_PLATFORM」*2から始まるif文で,実行環境がWindowsであるかを判別し,もしそうなら,実行ファイルに「.exe」をつけています.Cygwin限定かもしれませんが,「.exe」をつけていないと,test関数によるファイルの存在確認ができないのです.
  • 「0.upto(x) do |i|」は,「(x + 1).times do |i|」と等価*3ですが,ブロックパラメータを用いるtimesはどうもなじめません.

Cで書き換えて*4,Cプログラミングの授業で「ソースコードを生成するソースコード」の例にするのも,いいなあ….