抽象構文木ジェネレータ

こんな感じ

Expr:
  PlusExpr:
    rhs: Expr
    lhs: Expr
  MulExpr:
    rhs: Expr
    lhs: Expr
  MinusExpr:
    rhs: Expr
    lhs: Expr
  DivExpr:
    rhs: Expr
    lhs: Expr
  IntExpr:
    value: int

で、抽象構文木にしたいのを書いて、

$ ruby astgen.rb hoge.huga input.yaml

とかやると、ざざーっとクラス定義とVisitorを生成してくれるやつを作りました。勝手にPositionとか言うクラスを作って、開始位置終了位置ファイル名を保持するようになってるので、抽象構文木以外に使えるかどうかは、微妙。

ていうか、これ前も書いた気がするんだけどなぁ………どこやったんだろ。今日は再発明ばっかりだぜ。

インタフェースと実装クラスはpackageを分けるほうが良いような気がしますが、実装がめんどくさい、のは置いておいても、package名のエイリアスくらいできないといちいち完全修飾名で書かないといけないようなことにすぐなるような気がするので、それが嫌な感じ。inner classでなんとかできないかと思ってちょっと試したんだけど、Javaのinner classってそういう用途には使わないのか。

※みずしまくんに聞いてみたら、「そういうときは、staticなinner classを使うんですよー」とか教えてくれたので、直した。これでnamespaceがすっきり。なるほど。staticってそういう意味だったのか。

require 'optparse'
require 'pp'
require 'pathname'
require 'yaml'

class HelpException < RuntimeError
end

def format_type(typename, class_name)
  typename.gsub(/\bself\b/, class_name).gsub(/\bList\b/, "java.util.List")
end

def write_position(io, package_name)
  io.puts <<EOS
package #{package_name};

public class Position {
\tpublic final int startLine;
\tpublic final int startColumn;

\tpublic final int endLine;
\tpublic final int endColumn;

\tpublic final java.io.File file;

\tpublic Position(int startLine, int startColumn, int endLine, int endColumn, java.io.File file) {
\t\tthis.startLine = startLine;
\t\tthis.startColumn = startColumn;
\t\tthis.endLine = endLine;
\t\tthis.endColumn = endColumn;
\t\tthis.file = file;
\t}

\tpublic String toString() {
\t\t return (String.format("File \\"%s\\", line %d-%d, characters %d-%d",
\t\t\t\tthis.file,
\t\t\t\tthis.startLine, this.endLine,
\t\t\t\tthis.startColumn, this.endColumn));
\t}
}

EOS
end
def write_interface(io, package_name, class_name, children)
  io.puts "package #{package_name};"
  io.puts
  io.puts "public abstract class #{class_name} {"
  io.puts "\tpublic Position position;"
  io.puts 
  io.puts "\tpublic #{class_name}(Position position) {"
  io.puts "\t\tthis.position = position;"
  io.puts "\t}"
  io.puts
  io.puts "\tpublic abstract <T> T visit(#{class_name}Visitor<T> visitor);"
  io.puts 
  children.each {|child_name, members|
    write_class(io, package_name, class_name, child_name, members);
    io.puts
  }
  io.puts "}"
end

def write_class(io, package_name, super_class, class_name, members)
  io.puts "\tpublic static class #{class_name} extends #{super_class} {"
  members.each {|name, type|
    io.puts "\t\tpublic #{format_type(type, class_name)} #{name};"
  }
  io.puts
  
  constr_params = (["Position position"] +
                   members.keys.collect {|name|
                     "#{format_type(members[name], class_name)} #{name}"
                   }).join(', ')
  io.puts "\t\tpublic #{class_name}(#{constr_params}) {"
  io.puts "\t\t\tsuper(position);"
  members.keys.each {|name|
    io.puts "\t\t\tthis.#{name} = #{name};"
  }
  io.puts "\t\t}"
  io.puts
  
  io.puts "\t\t@Override"
  io.puts "\t\tpublic <T> T visit(#{super_class}Visitor<T> visitor) {"
  io.puts "\t\t\treturn visitor.accept(this);"
  io.puts "\t\t}"
  io.puts "\t}"
end

def write_visitor(io, package_name, class_name, children)
  io.puts "package #{package_name};"
  io.puts
  io.puts "public interface #{class_name}Visitor<T> {"
  children.each {|c|
    object_name = c[0,1].downcase + c[1,c.length]
    io.puts "\tT accept(#{class_name}.#{c} #{object_name});"
  }
  io.puts "}"
end

opt = OptionParser.new()
dir = Pathname(".")

begin
  opt.on('-d [dir]') {|path| dir = Pathname(path) }
  opt.banner = "Usage: astgen [options] package_name input.yaml"
  opt.parse!
  
  raise HelpException unless ARGV.length == 2
  
  package_name = ARGV[0]
  input = YAML.load_file(Pathname(ARGV[1]))
  srcdir = package_name.split(".").inject(dir) {|path, dir|
    newpath = path + dir
    newpath.mkdir unless newpath.exist?
    newpath
  }
  
  puts "output dir: #{srcdir}"
  puts "package name: #{package_name}"
  puts "input file: #{ARGV[1]}"
  
  (srcdir+"Position.java").open('w') {|io|
    write_position(io, package_name)
  }
  
  input.each {|k,v|
    interface_file = srcdir + "#{k}.java"
    interface_file.open('w') {|io|
      write_interface(io, package_name, k, v)
    }

    (srcdir+"#{k}Visitor.java").open('w') {|io|
      write_visitor(io, package_name, k, v.keys)
    }
  }
rescue HelpException, OptionParser::InvalidOption
  puts opt
end