mealy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []