mealy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a060d9e9e21e6a51b0116e42b315e0a488b8ea18a6ca8571e3162671123186c
4
+ data.tar.gz: 00e93453f2f5b13a7d568962c1f604f31ad32099c45b540191e5e93f3bf47b42
5
+ SHA512:
6
+ metadata.gz: 1c8aec8689484fbeba152dd3a2b8f4c7db439a8635e3a3e18e9e76b0417bf8fea7eb7cf302824443ff5a5fb0f3f5e4ac822a90a7e35e420c725d0924c13d3002
7
+ data.tar.gz: af39ce4307ff5962c921730392916d89408c582f56a8d22d4823f5eb49552bbba7f3301bdc4fa5a41815c6957153c509bb8fb0f63fb3e5a0023342d360f9797c
@@ -0,0 +1,43 @@
1
+ require_relative 'mealy/dsl'
2
+ require_relative 'mealy/label'
3
+ require_relative 'mealy/runner'
4
+
5
+ # A Mealy finite state machine.
6
+ #
7
+ # For usage information please read {file:README.md README}.
8
+ module Mealy
9
+ # Error indicating that there is no transition from the current state with
10
+ # the token read.
11
+ class UnexpectedTokenError < StandardError
12
+ def initialize(state, on)
13
+ super("FSM error #{self.class} in state #{state.inspect} reading #{on}")
14
+ end
15
+ end
16
+
17
+ # emit tokens from the DSL blocks
18
+ # @param token the emitted token
19
+ def emit(token)
20
+ return unless @emit_runner
21
+ @emit_runner.emit(token)
22
+ end
23
+
24
+ # Runs the Mealy machine on the given input. Outputs a stream of tokens by
25
+ # yielding each emitted token to the given block.
26
+ # @param enum [Enumerable] the input for the FSM
27
+ # @return [Enumerator] if no block is given
28
+ # @yieldparam emit The user token emitted by {#emit}.
29
+ def run(enum, &block)
30
+ return to_enum(:run, enum) unless block_given?
31
+
32
+ @emit_runner = Runner.new(self)
33
+ @emit_runner.run(enum, &block)
34
+ @emit_runner = nil
35
+ end
36
+
37
+ # Runs the Mealy machine on the given input.
38
+ # @param enum [Enumerable] the input for the FSM
39
+ # @return the return value of the {Mealy::DSL#finish} block.
40
+ def execute(enum)
41
+ Executer.new(self).run(enum)
42
+ end
43
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'label'
4
+ require_relative 'helper_methods'
5
+
6
+ module Mealy
7
+ # The class level DSL for defining machines.
8
+ module DSL
9
+ # Declares the initial state of the FSM.
10
+ # @param sym [Symbol] the initial state
11
+ # @param block user code executed in the instance of the FSM instance on
12
+ # start up
13
+ def initial_state(sym, &block)
14
+ @start_data = [sym, block]
15
+ end
16
+
17
+ # An FSM transition.
18
+ # @param from [Array|Symbol] the state or Array of states we transition
19
+ # away from
20
+ # @param to [Symbol] the state we transition to
21
+ # @param on [Label] only allows this rule to trigger if the read
22
+ # token matches ({HelperMethods.Label} is automatically called
23
+ # on this)
24
+ # @param block user code executed when the rule fires
25
+ # @yieldparam input The read input, that matches the rules {Label}
26
+ # @yieldparam from The state we are transitioning away from
27
+ # @yieldparam to The state we are transitioning to
28
+ def transition(from:, to:, on: ANY, &block)
29
+ hash = { HelperMethods.Label(on) => { to: to, block: block } }
30
+ [* from].each do |origin|
31
+ @transitions[origin] = @transitions[origin].merge(hash)
32
+ end
33
+ end
34
+
35
+ # An FSM loop
36
+ # @param state [Array|Symbol] the state or states we loop on
37
+ # @param on [Label] only allows this rule to trigger if the read
38
+ # token matches ({HelperMethods.Label} is automatically called
39
+ # on this)
40
+ # @param block user code executed on each iteration of the loop
41
+ def read(state:, on: ANY, &block)
42
+ [* state].each do |one_state|
43
+ transition(from: one_state, to: one_state, on: on, &block)
44
+ end
45
+ end
46
+
47
+ # final FSM state
48
+ # @param block fires on FSM shutdown
49
+ def finish(&block)
50
+ @finish_data = block
51
+ end
52
+ end
53
+
54
+ # @private
55
+ # Module.included hook. Resets the state transitions for a class
56
+ def self.included(klass)
57
+ klass.class_eval { @transitions = Hash.new { Hash.new({}) } }
58
+ klass.extend(DSL)
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'label'
4
+
5
+ module Mealy
6
+ # Various helper methods
7
+ module HelperMethods
8
+ # Converts types to Labels. {Mealy::DSL#transition} calls this to convert
9
+ # anything to a {Label}.
10
+ # @return [Label]
11
+ def self.Label(convertee)
12
+ if convertee.kind_of?(Label) then convertee
13
+ else Label.new(convertee)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Mealy
6
+ # FSM state transition arrow labels. In effect we match the input tokens
7
+ # against the labels to decide which transition to take.
8
+ class Label
9
+ # @param label Something that can be tested with input tokens
10
+ def initialize(label)
11
+ @label = label
12
+ end
13
+
14
+ # @param input Something that can match label
15
+ def match?(input)
16
+ @label === input
17
+ end
18
+ end
19
+
20
+ # Singleton for a Label that matches anything. See {ANY}.
21
+ class AnyLabel < Label
22
+ include Singleton
23
+
24
+ def initialize
25
+ end
26
+
27
+ private :initialize
28
+
29
+ # ignores any input and matches.
30
+ def match?(*_)
31
+ true
32
+ end
33
+ end
34
+
35
+ # Wildcard for machine input tokens that match anything.
36
+ ANY = AnyLabel.instance
37
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mealy
4
+ # @api private
5
+ #
6
+ # An object on which {#run} behaves like Mealy{Mealy#execute}. The internal state
7
+ # is tracked by this instance, the user state is in {Mealy}.
8
+ class Executer
9
+ # @param mealy [Mealy] mealy instance
10
+ def initialize(mealy)
11
+ @mealy = mealy
12
+ @state = nil
13
+ end
14
+
15
+ # same as calling {Mealy#execute}
16
+ def run(enum)
17
+ start
18
+
19
+ enum.each { |c| run_for_token(c) }
20
+
21
+ finish
22
+ end
23
+
24
+ private
25
+
26
+ def start
27
+ @state, block = start_data
28
+ user_action(block)
29
+ end
30
+
31
+ def run_for_token(token)
32
+ params = lookup_transition_for(token)
33
+ block = params[:block]
34
+ from = @state
35
+ to = params[:to]
36
+ @state = to
37
+ user_action(block, token, from, to)
38
+ end
39
+
40
+ def finish
41
+ user_action(finish_data)
42
+ end
43
+
44
+ def lookup_transition_for(char)
45
+ on_not_found = -> { raise UnexpectedTokenError.new(@state, char) }
46
+ _, params = transitions[@state].find(on_not_found) do |key, _|
47
+ key.match?(char)
48
+ end
49
+ params
50
+ end
51
+
52
+ def user_action(user_action_block, *args)
53
+ return if user_action_block.nil?
54
+
55
+ @mealy.instance_exec(*args, &user_action_block)
56
+ end
57
+
58
+ %i[start_data transitions finish_data].each do |sym|
59
+ define_method(sym) do
60
+ @mealy.class.instance_variable_get(:"@#{sym}")
61
+ end
62
+ end
63
+ end
64
+
65
+ # @api private
66
+ #
67
+ # Extends {Executer} with emitting capabilities.
68
+ class Runner < Executer
69
+ # emit tokens from the DSL blocks
70
+ # @param emit the emitted token
71
+ def emit(emit)
72
+ @emit_block.call(emit)
73
+ end
74
+
75
+ # same as calling {Mealy#run}
76
+ def run(enum, &emit_block)
77
+ @emit_block = emit_block
78
+
79
+ super
80
+ end
81
+ end
82
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mealy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Sonkoly
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: simplecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: |
42
+ An easy on the eye DSL to define Mealy FSMs. Can be used for lexers, stream
43
+ transformers etc.
44
+ email: sonkoly.pal@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - lib/mealy.rb
50
+ - lib/mealy/dsl.rb
51
+ - lib/mealy/helper_methods.rb
52
+ - lib/mealy/label.rb
53
+ - lib/mealy/runner.rb
54
+ homepage: http://github.com/phaul/mealy
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.7.7
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: A ruby DSL to create mealy state machines.
78
+ test_files: []