breily / states

Ruby library for creating finite state machines

states / dfa.rb
100644 111 lines (105 sloc) 3.135 kb
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
class DFA
    attr_accessor :states, :currentstate, :match
    def initialize(states=[])
        @states = states
        @currentstate = states[0]
        @match = []
    end
    def give input
        # Can't match if state is nil
        if @currentstate.nil? then puts "** [#{input}] Current state is nil"
        else
            # Call the specified entry action
            @currentstate.onexit.call input
            # Find the next state (match returns state name, so we use find)
            @currentstate = @states.find { |s| s.name == @currentstate.match(input) }
            # Call the specified exit action, as long as the state isn't nil
            @currentstate.onentry.call input unless @currentstate.nil?
        end
        # If the input matched, record it
        @match.push input unless @currentstate.nil?
        @currentstate
    end
    # Call give on each member of a string
    # See givea return values
    def gives input
        givea input.split ''
    end
    # Call give on each member of an array
    # Returns false if input doesn't match
    # Otherwise returns list of states the machine went through
    def givea input
        input.collect do |item|
            st = give item
            return false if st.nil?
            st
        end
    end
    def inspect
        return "<DFA: [#{@states.collect { |s| s.name }.join ', '}]>"
    end
    # Will output Ruby to match this DFA
    def render
        puts "def match_dfa(input)"
    end
end
 
class State
    attr_accessor :name, :transitions, :onexit, :onentry, :final
    def initialize(name, transitions={})
        @name = name
        @transitions = transitions
        @onentry = proc {}
        @onexit = proc {}
        @final = false
    end
    def match input
        ret = @transitions[input]
        ret = @transitions[:dot] if ret.nil? and @transitions.keys.include? :dot
        raise ":error state reached with '#{input}'" if ret == :error
        return ret
    end
    def inspect
        return "<State: #{@name} :: #{@transitions.inspect}>"
    end
    def transition(params)
        @transitions.update params
    end
    def on_exit(&block)
        @onexit = block
    end
    def on_enter(&block)
        @onentry = block
    end
    alias :old_eq :==
    def ==(other)
        if other.class == String
            @name == other
        else
            old_eq(other)
        end
    end
end
 
####
# Utility function to define States/Machines
####
 
def state(name, *args, &block)
    s = State.new(name)
    s.final = true if args.include? "final" or args.include? :final
    s.instance_eval &block if block_given?
    s
end
 
# Adds status messages as entry and exit actions
def verbose_state(name, transitions)
    s = State.new(name, transitions)
    s.on_exit { |c| puts "+ input[#{c}] >> enter state[#{@name}]" }
    s.on_enter { |c| puts "- exit state[#{@name}] >> input[#{c}]" }
end
 
def automaton *args
    states = []
    until args.empty?
        a, b = args.take 2
        states.push State.new(a, b)
        args = args.drop 2
    end
    return DFA.new(states)
end