summaryrefslogtreecommitdiff
path: root/ext/ripper/tools/dsl.rb
blob: 38f859dd97ed61d7d2c77f0be396d45982c7b6e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# frozen_string_literal: true

# Simple DSL implementation for Ripper code generation
#
# input: /*% ripper: stmts_add!(stmts_new!, void_stmt!) %*/
# output:
#   VALUE v1, v2;
#   v1 = dispatch0(stmts_new);
#   v2 = dispatch0(void_stmt);
#   $$ = dispatch2(stmts_add, v1, v2);
#
# - The code must be a single line.
#
# - The code is basically Ruby code, even if it appears like in C and
#   the result will be processed as C. e.g., comments need to be in
#   Ruby style.

class DSL
  TAG_PATTERN = /(?><[a-zA-Z0-9_]+>)/.source
  NAME_PATTERN = /(?>\$|\d+|[a-zA-Z_][a-zA-Z0-9_]*|\[[a-zA-Z_.][-a-zA-Z0-9_.]*\])(?>(?:\.|->)[a-zA-Z_][a-zA-Z0-9_]*)*/.source
  NOT_REF_PATTERN = /(?>\#.*|[^\"$@]*|"(?>\\.|[^\"])*")/.source

  def self.line?(line, lineno = nil, indent: nil)
    if %r<(?<space>\s*)/\*% *ripper(?:\[(?<option>.*?)\])?: *(?<code>.*?) *%\*/> =~ line
      new(code, comma_split(option), lineno, indent: indent || space)
    end
  end

  def self.comma_split(str)
    str or return []
    str.scan(/(([^(,)]+|\((?:,|\g<0>)*\))+)/).map(&:first)
  end

  using Module.new {
    refine Array do
      def to_s
        if empty?
          "rb_ary_new()"
        else
          "rb_ary_new_from_args(#{size}, #{map(&:to_s).join(', ')})"
        end
      end
    end
  }

  class Var
    class Table < Hash
      def initialize(&block)
        super() {|tbl, arg|
          tbl.fetch(arg, &block)
        }
      end

      def fetch(arg, &block)
        super {
          self[arg] = Var.new(self, arg, &block)
        }
      end

      def add(&block)
        v = new_var
        self[v] = Var.new(self, v, &block)
      end

      def defined?(name)
        name = name.to_s
        any? {|_, v| v.var == name}
      end

      def new_var
        "v#{size+1}"
      end
    end

    attr_reader :var, :value

    PRETTY_PRINT_INSTANCE_VARIABLES = instance_methods(false).freeze

    def pretty_print_instance_variables
      PRETTY_PRINT_INSTANCE_VARIABLES
    end

    alias to_s var

    def initialize(table, arg, &block)
      @var = table.new_var
      @value = yield arg
      @table = table
    end

    # Indexing.
    #
    #   $:1 -> v1=get_value($:1)
    #   $:1[0] -> rb_ary_entry(v1, 0)
    #   $:1[0..1] -> [rb_ary_entry(v1, 0), rb_ary_entry(v1, 1)]
    #   *$:1[0..1] -> rb_ary_entry(v1, 0), rb_ary_entry(v1, 1)
    #
    # Splat needs `[range]` because `Var` does not have the length info.
    def [](idx)
      if ::Range === idx
        idx.map {|i| self[i]}
      else
        @table.fetch("#@var[#{idx}]") {"rb_ary_entry(#{@var}, #{idx})"}
      end
    end
  end

  def initialize(code, options, lineno = nil, indent: "\t\t\t")
    @lineno = lineno
    @indent = indent
    @events = {}
    @error = options.include?("error")
    if options.include?("final")
      @final = "p->result"
    else
      @final = (options.grep(/\A\$#{NAME_PATTERN}\z/o)[0] || "p->s_lvalue")
    end

    bind = dsl_binding
    @var_table = Var::Table.new {|arg| "get_value(#{arg})"}
    code = code.gsub(%r[\G#{NOT_REF_PATTERN}\K(\$|\$:|@)#{TAG_PATTERN}?#{NAME_PATTERN}]o) {
      if (arg = $&) == "$:$"
        '"p->s_lvalue"'
      elsif arg.start_with?("$:")
        "(#{@var_table[arg]}=@var_table[#{arg.dump}])"
      else
        arg.dump
      end
    }
    @last_value = bind.eval(code)
  rescue SyntaxError
    $stderr.puts "error on line #{@lineno}" if @lineno
    raise
  end

  def dsl_binding(p = "p")
    # struct parser_params *p
    binding
  end

  attr_reader :events

  undef lambda
  undef hash
  undef :class

  def generate
    s = "#@final=#@last_value;"
    s << "ripper_error(p);" if @error
    unless @var_table.empty?
      vars = @var_table.map {|_, v| "#{v.var}=#{v.value}"}.join(", ")
      s = "VALUE #{ vars }; #{ s }"
    end
    "#{@indent}{#{s}}"
  end

  def add_event(event, args)
    event = event.to_s.sub(/!\z/, "")
    @events[event] = args.size
    vars = []
    args.each do |arg|
      arg = @var_table.add {arg} unless Var === arg
      vars << arg
    end
    @var_table.add {"dispatch#{ args.size }(#{ [event, *vars].join(",") })"}
  end

  def method_missing(event, *args)
    if event.to_s =~ /!\z/
      add_event(event, args)
    elsif args.empty? and (/\Aid[A-Z_]/ =~ event or @var_table.defined?(event))
      event
    else
      "#{ event }(#{ args.map(&:to_s).join(", ") })"
    end
  end

  def self.const_missing(name)
    name
  end
end