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.
- checksums.yaml +7 -0
- data/lib/mealy.rb +43 -0
- data/lib/mealy/dsl.rb +60 -0
- data/lib/mealy/helper_methods.rb +17 -0
- data/lib/mealy/label.rb +37 -0
- data/lib/mealy/runner.rb +82 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/mealy.rb
ADDED
@@ -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
|
data/lib/mealy/dsl.rb
ADDED
@@ -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
|
data/lib/mealy/label.rb
ADDED
@@ -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
|
data/lib/mealy/runner.rb
ADDED
@@ -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: []
|