summaryrefslogtreecommitdiff
path: root/ext/ripper/tools/dsl.rb
blob: 3e368813e516d84313205d25b0737a354467eb3a (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
# 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 initialize(code, options, lineno = nil)
    @lineno = lineno
    @events = {}
    @error = options.include?("error")
    @brace = options.include?("brace")
    if options.include?("final")
      @final = "p->result"
    else
      @final = (options.grep(/\A\$#{NAME_PATTERN}\z/o)[0] || "p->s_lvalue")
    end
    @vars = 0

    # struct parser_params *p
    p = p = "p"

    @code = +""
    code = code.gsub(%r[\G#{NOT_REF_PATTERN}\K(\$|\$:|@)#{TAG_PATTERN}?#{NAME_PATTERN}]o, '"\&"')
    @last_value = eval(code)
  rescue SyntaxError
    $stderr.puts "error on line #{@lineno}" if @lineno
    raise
  end

  attr_reader :events

  undef lambda
  undef hash
  undef class

  def generate
    s = "#@code#@final=#@last_value;"
    s = "{VALUE #{ (1..@vars).map {|v| "v#{ v }" }.join(",") };#{ s }}" if @vars > 0
    s << "ripper_error(p);" if @error
    s = "{#{ s }}" if @brace
    "\t\t\t#{s}"
  end

  def new_var
    "v#{ @vars += 1 }"
  end

  def opt_event(event, default, addend)
    add_event(event, [default, addend], true)
  end

  def add_event(event, args, qundef_check = false)
    event = event.to_s.sub(/!\z/, "")
    @events[event] = args.size
    vars = []
    args.each do |arg|
      vars << v = new_var
      if arg =~ /\A\$:#{NAME_PATTERN}\z/
        @code << "#{ v }=get_value(#{arg});"
      else
        @code << "#{ v }=#{ arg };"
      end
    end
    v = new_var
    d = "dispatch#{ args.size }(#{ [event, *vars].join(",") })"
    d = "#{ vars.last }==rb_ripper_none ? #{ vars.first } : #{ d }" if qundef_check
    @code << "#{ v }=#{ d };"
    v
  end

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

  def self.const_missing(name)
    name
  end
end