From 12b8efc801ba6ae354dbba3874945deca5445159 Mon Sep 17 00:00:00 2001 From: Alex Kotov Date: Thu, 16 Sep 2021 13:35:43 +0500 Subject: [PATCH] Include gem "morpher" --- Gemfile.lock | 18 -- lib/morpher.rb | 111 +++++++++ lib/morpher/compiler.rb | 17 ++ lib/morpher/compiler/emitter.rb | 82 ++++++ lib/morpher/compiler/error.rb | 84 +++++++ lib/morpher/compiler/evaluator.rb | 63 +++++ lib/morpher/compiler/evaluator/emitter.rb | 224 +++++++++++++++++ lib/morpher/compiler/preprocessor.rb | 29 +++ lib/morpher/compiler/preprocessor/emitter.rb | 54 ++++ .../compiler/preprocessor/emitter/anima.rb | 69 ++++++ .../compiler/preprocessor/emitter/boolean.rb | 31 +++ .../compiler/preprocessor/emitter/key.rb | 87 +++++++ .../compiler/preprocessor/emitter/noop.rb | 45 ++++ .../compiler/preprocessor/emitter/param.rb | 50 ++++ lib/morpher/evaluation.rb | 118 +++++++++ lib/morpher/evaluator.rb | 40 +++ lib/morpher/evaluator/binary.rb | 46 ++++ lib/morpher/evaluator/nary.rb | 97 ++++++++ lib/morpher/evaluator/nullary.rb | 92 +++++++ .../evaluator/nullary/parameterized.rb | 48 ++++ lib/morpher/evaluator/predicate.rb | 22 ++ lib/morpher/evaluator/predicate/boolean.rb | 76 ++++++ .../evaluator/predicate/contradiction.rb | 36 +++ lib/morpher/evaluator/predicate/eql.rb | 50 ++++ lib/morpher/evaluator/predicate/negation.rb | 52 ++++ lib/morpher/evaluator/predicate/primitive.rb | 49 ++++ lib/morpher/evaluator/predicate/tautology.rb | 36 +++ lib/morpher/evaluator/transformer.rb | 75 ++++++ .../evaluator/transformer/attribute.rb | 25 ++ lib/morpher/evaluator/transformer/block.rb | 81 ++++++ lib/morpher/evaluator/transformer/coerce.rb | 166 +++++++++++++ lib/morpher/evaluator/transformer/custom.rb | 34 +++ lib/morpher/evaluator/transformer/domain.rb | 86 +++++++ .../transformer/domain/attribute_accessors.rb | 60 +++++ .../transformer/domain/attribute_hash.rb | 52 ++++ .../transformer/domain/instance_variables.rb | 60 +++++ .../evaluator/transformer/domain/param.rb | 54 ++++ lib/morpher/evaluator/transformer/guard.rb | 62 +++++ .../evaluator/transformer/hash_transform.rb | 149 +++++++++++ lib/morpher/evaluator/transformer/input.rb | 37 +++ lib/morpher/evaluator/transformer/key.rb | 86 +++++++ lib/morpher/evaluator/transformer/map.rb | 100 ++++++++ lib/morpher/evaluator/transformer/merge.rb | 25 ++ lib/morpher/evaluator/transformer/static.rb | 27 ++ lib/morpher/evaluator/unary.rb | 79 ++++++ lib/morpher/node_helpers.rb | 19 ++ lib/morpher/printer.rb | 233 ++++++++++++++++++ lib/morpher/printer/mixin.rb | 58 +++++ lib/morpher/registry.rb | 51 ++++ lib/morpher/type_lookup.rb | 51 ++++ mutant.gemspec | 1 - 51 files changed, 3378 insertions(+), 19 deletions(-) create mode 100644 lib/morpher.rb create mode 100644 lib/morpher/compiler.rb create mode 100644 lib/morpher/compiler/emitter.rb create mode 100644 lib/morpher/compiler/error.rb create mode 100644 lib/morpher/compiler/evaluator.rb create mode 100644 lib/morpher/compiler/evaluator/emitter.rb create mode 100644 lib/morpher/compiler/preprocessor.rb create mode 100644 lib/morpher/compiler/preprocessor/emitter.rb create mode 100644 lib/morpher/compiler/preprocessor/emitter/anima.rb create mode 100644 lib/morpher/compiler/preprocessor/emitter/boolean.rb create mode 100644 lib/morpher/compiler/preprocessor/emitter/key.rb create mode 100644 lib/morpher/compiler/preprocessor/emitter/noop.rb create mode 100644 lib/morpher/compiler/preprocessor/emitter/param.rb create mode 100644 lib/morpher/evaluation.rb create mode 100644 lib/morpher/evaluator.rb create mode 100644 lib/morpher/evaluator/binary.rb create mode 100644 lib/morpher/evaluator/nary.rb create mode 100644 lib/morpher/evaluator/nullary.rb create mode 100644 lib/morpher/evaluator/nullary/parameterized.rb create mode 100644 lib/morpher/evaluator/predicate.rb create mode 100644 lib/morpher/evaluator/predicate/boolean.rb create mode 100644 lib/morpher/evaluator/predicate/contradiction.rb create mode 100644 lib/morpher/evaluator/predicate/eql.rb create mode 100644 lib/morpher/evaluator/predicate/negation.rb create mode 100644 lib/morpher/evaluator/predicate/primitive.rb create mode 100644 lib/morpher/evaluator/predicate/tautology.rb create mode 100644 lib/morpher/evaluator/transformer.rb create mode 100644 lib/morpher/evaluator/transformer/attribute.rb create mode 100644 lib/morpher/evaluator/transformer/block.rb create mode 100644 lib/morpher/evaluator/transformer/coerce.rb create mode 100644 lib/morpher/evaluator/transformer/custom.rb create mode 100644 lib/morpher/evaluator/transformer/domain.rb create mode 100644 lib/morpher/evaluator/transformer/domain/attribute_accessors.rb create mode 100644 lib/morpher/evaluator/transformer/domain/attribute_hash.rb create mode 100644 lib/morpher/evaluator/transformer/domain/instance_variables.rb create mode 100644 lib/morpher/evaluator/transformer/domain/param.rb create mode 100644 lib/morpher/evaluator/transformer/guard.rb create mode 100644 lib/morpher/evaluator/transformer/hash_transform.rb create mode 100644 lib/morpher/evaluator/transformer/input.rb create mode 100644 lib/morpher/evaluator/transformer/key.rb create mode 100644 lib/morpher/evaluator/transformer/map.rb create mode 100644 lib/morpher/evaluator/transformer/merge.rb create mode 100644 lib/morpher/evaluator/transformer/static.rb create mode 100644 lib/morpher/evaluator/unary.rb create mode 100644 lib/morpher/node_helpers.rb create mode 100644 lib/morpher/printer.rb create mode 100644 lib/morpher/printer/mixin.rb create mode 100644 lib/morpher/registry.rb create mode 100644 lib/morpher/type_lookup.rb diff --git a/Gemfile.lock b/Gemfile.lock index 5f338c49..eb9173f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,6 @@ PATH equalizer (~> 0.0.9) ice_nine (~> 0.11.1) memoizable (~> 0.4.2) - morpher (~> 0.3.1) parser (~> 2.7.2) procto (~> 0.0.2) regexp_parser (~> 1.8.2) @@ -87,23 +86,6 @@ GEM kwalify (0.7.2) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) - morpher (0.3.1) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - anima (~> 0.3.0) - ast (~> 2.2) - concord (~> 0.1.5) - equalizer (~> 0.0.9) - ice_nine (~> 0.11.0) - mprelude (~> 0.1.0) - procto (~> 0.0.2) - mprelude (0.1.0) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - concord (~> 0.1.5) - equalizer (~> 0.0.9) - ice_nine (~> 0.11.1) - procto (~> 0.0.2) parallel (1.12.1) parser (2.7.2.0) ast (~> 2.4.1) diff --git a/lib/morpher.rb b/lib/morpher.rb new file mode 100644 index 00000000..a6535547 --- /dev/null +++ b/lib/morpher.rb @@ -0,0 +1,111 @@ +require 'abstract_type' +require 'concord' +require 'anima' +require 'ast' +require 'procto' + +# Library namespace module +module Morpher + + Undefined = Module.new.freeze + + # Return evaluator from node + # + # @param [Node] + # + # @return [Evaluator] + # + # @api private + # + def self.compile(node) + node = Compiler::Preprocessor::DEFAULT.call(node) + Compiler::Evaluator::DEFAULT.call(node) + end + + # Return evaluate block to produce an AST node + # + # @return [AST::Node] + # + # @api private + # + def self.sexp(&block) + NodeHelpers.module_eval(&block) + end + + # Build morpher from yielding sexp blog + # + # @return [Evaluator] + # + # @api private + # + def self.build(&block) + compile(sexp(&block)) + end + +end # Morpher + +require 'morpher/node_helpers' +require 'morpher/registry' +require 'morpher/printer' +require 'morpher/printer/mixin' +require 'morpher/evaluator' +require 'morpher/evaluator/nullary' +require 'morpher/evaluator/nullary/parameterized' +require 'morpher/evaluator/unary' +require 'morpher/evaluator/binary' +require 'morpher/evaluator/nary' +require 'morpher/evaluator/transformer' +require 'morpher/evaluator/transformer/block' +require 'morpher/evaluator/transformer/key' +require 'morpher/evaluator/transformer/guard' +require 'morpher/evaluator/transformer/attribute' +require 'morpher/evaluator/transformer/hash_transform' +require 'morpher/evaluator/transformer/map' +require 'morpher/evaluator/transformer/static' +require 'morpher/evaluator/transformer/input' +require 'morpher/evaluator/transformer/merge' +require 'morpher/evaluator/transformer/coerce' +require 'morpher/evaluator/transformer/custom' +require 'morpher/evaluator/transformer/domain' +require 'morpher/evaluator/transformer/domain/param' +require 'morpher/evaluator/transformer/domain/attribute_hash' +require 'morpher/evaluator/transformer/domain/instance_variables' +require 'morpher/evaluator/transformer/domain/attribute_accessors' +require 'morpher/evaluator/predicate' +require 'morpher/evaluator/predicate/eql' +require 'morpher/evaluator/predicate/primitive' +require 'morpher/evaluator/predicate/negation' +require 'morpher/evaluator/predicate/tautology' +require 'morpher/evaluator/predicate/contradiction' +require 'morpher/evaluator/predicate/boolean' +require 'morpher/evaluation' +require 'morpher/evaluation' +require 'morpher/type_lookup' +require 'morpher/compiler' +require 'morpher/compiler/error' +require 'morpher/compiler/emitter' +require 'morpher/compiler/evaluator' +require 'morpher/compiler/evaluator/emitter' +require 'morpher/compiler/preprocessor' +require 'morpher/compiler/preprocessor/emitter' +require 'morpher/compiler/preprocessor/emitter/noop' +require 'morpher/compiler/preprocessor/emitter/key' +require 'morpher/compiler/preprocessor/emitter/param' +require 'morpher/compiler/preprocessor/emitter/boolean' +require 'morpher/compiler/preprocessor/emitter/anima' + +module Morpher + class Compiler + + class Preprocessor + # Default preprocessor compiler + DEFAULT = new(Emitter::REGISTRY.freeze) + end # Preprocessor + + class Evaluator + # Default evaluator compiler + DEFAULT = new(Morpher::Evaluator::REGISTRY, Emitter::REGISTRY.freeze) + end # Evaluator + + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler.rb b/lib/morpher/compiler.rb new file mode 100644 index 00000000..132aaa73 --- /dev/null +++ b/lib/morpher/compiler.rb @@ -0,0 +1,17 @@ +module Morpher + # Abstract compiler base class + class Compiler + include AbstractType + + # Call compiler + # + # @param [Node] node + # + # @return [Object] + # + # @api private + # + abstract_method :call + + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/emitter.rb b/lib/morpher/compiler/emitter.rb new file mode 100644 index 00000000..5ab6e211 --- /dev/null +++ b/lib/morpher/compiler/emitter.rb @@ -0,0 +1,82 @@ +module Morpher + class Compiler + # Abstract target indepentand emitter + class Emitter + include AbstractType, Adamantium::Flat, NodeHelpers, Procto.call(:output) + extend NodeHelpers + + # Return output of emitter + # + # @return [Object] + # + # @api private + # + abstract_method :output + + # Return node + # + # @return [Node] + # + # @api private + # + abstract_method :node + private :node + + private + + # Return children + # + # @return [Array] + # + # @api private + # + def children + node.children + end + + # Assert number of child nodes + # + # @return [self] + # if assertion is fullfilled + # + # @raise [NodeError] + # otherwise + # + # @api private + # + def assert_children_amount(expected_amount) + actual_amount = children.length + fail Error::NodeChildren.new(node, expected_amount) unless actual_amount.equal?(expected_amount) + end + + # Name children + # + # @return [undefined] + # + # @api private + # + # rubocop:disable MethodLength + # + def self.children(*names) + names.each_with_index do |name, index| + define_method(name) do + children.at(index) + end + private name + end + + define_method(:named_children) do + names + end + private :named_children + + define_method(:remaining_children) do + children.drop(names.length) + end + private :remaining_children + end + private_class_method :children + + end # Emitter + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/error.rb b/lib/morpher/compiler/error.rb new file mode 100644 index 00000000..188a1060 --- /dev/null +++ b/lib/morpher/compiler/error.rb @@ -0,0 +1,84 @@ +module Morpher + class Compiler + + # Abstract error class for compiler errors + class Error < RuntimeError + include AbstractType + + # Error raised when node children have incorrect amount + class NodeChildren < self + include Concord.new(:node, :expected_amount) + + # Return exception message + # + # @return [String] + # + # @api private + # + def message + "Expected #{expected_amount} #{_children} for #{type}, got #{actual_amount}: #{children}" + end + + private + + # Return inspected type + # + # @return [String] + # + # @api private + # + def type + node.type.inspect + end + + # Return actual amount of children + # + # @return [String] + # + # @api private + # + def actual_amount + children.length + end + + # Return children + # + # @return [Array] + # + # @api private + # + def children + node.children + end + + # Return user firendly children message + # + # @return [String] + # + # @api private + # + def _children + expected_amount.equal?(1) ? 'child' : 'children' + end + + end # NodeChildren + + # Error raised on compiling unknown nodes + class UnknownNode < self + include Concord.new(:type) + + # Return exception error message + # + # @return [String] + # + # @api private + # + def message + "Node type: #{type.inspect} is unknown" + end + + end # UnknownNode + + end # Error + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/evaluator.rb b/lib/morpher/compiler/evaluator.rb new file mode 100644 index 00000000..b8ca9ec3 --- /dev/null +++ b/lib/morpher/compiler/evaluator.rb @@ -0,0 +1,63 @@ +module Morpher + class Compiler + # Compiler with evaluators as output + class Evaluator < self + include Concord.new(:evaluators, :emitters) + + # Return evaluator tree for node + # + # @param [Node] node + # + # @return [Evalautor] + # on success + # + # @raise [Compiler::Error] + # on error + # + # @api private + # + def call(node) + evaluator = evaluator(node) + emitter = emitter(evaluator) + emitter.call(self, evaluator, node) + end + + private + + # Lookup evaluator for node + # + # @param [Node] + # + # @return [Class:Evaluator] + # if found + # + # @raise [Error::UnknownNode] + # otherwise + # + # @api private + # + def evaluator(node) + type = node.type + evaluators.fetch(type) do + fail Error::UnknownNode, type + end + end + + # Return emitter for evaluator + # + # @param [Class:Evalautor] + # + # @return [#call] + # + # @api private + # + def emitter(evaluator) + emitters.each do |arity, emitter| + return emitter if evaluator.ancestors.include?(arity) + end + fail Error::UnknownNode, evaluator + end + + end # Evaluator + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/evaluator/emitter.rb b/lib/morpher/compiler/evaluator/emitter.rb new file mode 100644 index 00000000..acf15c3f --- /dev/null +++ b/lib/morpher/compiler/evaluator/emitter.rb @@ -0,0 +1,224 @@ +module Morpher + class Compiler + class Evaluator + # Emitter for evaluators + class Emitter < Compiler::Emitter + include Registry, Concord.new(:compiler, :evaluator_klass, :node) + + # Return output + # + # @return [Evaluator] + # + # @api private + # + def output + validate_node + evaluator + end + memoize :output + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + abstract_method :validate_node + private :validate_node + + # Return evaluator + # + # @return [Evaluator] + # + # @api private + # + abstract_method :evaluator + private :evaluator + + # Emitter for nullary non parameterized evaluators + class Nullary < self + register Morpher::Evaluator::Nullary + + private + + # Return output + # + # @return [Evaluator] + # + # @api private + # + def evaluator + evaluator_klass.new + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + assert_children_amount(0) + end + + # Emitter for nullary parameterized evaluators + class Parameterized < self + register Morpher::Evaluator::Nullary::Parameterized + + children :param + + private + + # Return output + # + # @return [Evaluator] + # + # @api private + # + def evaluator + evaluator_klass.new(effective_param) + end + + # Return effective param + # + # @return [Object] + # + # @api private + # + def effective_param + if param.kind_of?(AST::Node) && param.type.equal?(:raw) && param.children.length.equal?(1) + param.children.first + else + param + end + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + assert_children_amount(1) + end + + end # Paramterized + end # Nullary + + # Emitter for unary evaluators + class Unary < self + register Morpher::Evaluator::Unary + children :operand + + private + + # Return evaluator + # + # @return [Evaluator] + # + # @api private + # + def evaluator + evaluator_klass.new(compiler.call(operand)) + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + assert_children_amount(1) + end + + end # Unary + + # Emitter for unary evaluators + class Binary < self + register Morpher::Evaluator::Binary + children :left, :right + + private + + # Return evaluator + # + # @return [Evaluator] + # + # @api private + # + def evaluator + evaluator_klass.new( + compiler.call(left), + compiler.call(right) + ) + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + assert_children_amount(2) + end + + end # Unary + + # Emitter for nary evaluators + class Nary < self + register Morpher::Evaluator::Nary + + private + + # Return evaluator + # + # @return [Evaluator] + # + # @api private + # + def evaluator + evaluator_klass.new(children.map(&compiler.method(:call))) + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + end + + end # Nary + + end # Emitter + end # Evaluator + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor.rb b/lib/morpher/compiler/preprocessor.rb new file mode 100644 index 00000000..0bf6d1fa --- /dev/null +++ b/lib/morpher/compiler/preprocessor.rb @@ -0,0 +1,29 @@ +module Morpher + class Compiler + # AST preprocessor + class Preprocessor < self + include Concord.new(:emitters) + + # Call preprocessor + # + # @param [Node] node + # the raw AST node after DSL + # + # @return [Node] + # the transformed ast node + # + # @api private + # + def call(node) + loop do + emitter = emitters.fetch(node.type, Emitter::Noop) + node = emitter.call(self, node) + break if emitter.equal?(Emitter::Noop) + end + + node + end + + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor/emitter.rb b/lib/morpher/compiler/preprocessor/emitter.rb new file mode 100644 index 00000000..3c761df9 --- /dev/null +++ b/lib/morpher/compiler/preprocessor/emitter.rb @@ -0,0 +1,54 @@ +module Morpher + class Compiler + class Preprocessor + # Abstract preprocessor emitter + class Emitter < Compiler::Emitter + include Registry, Concord.new(:preprocessor, :node) + + # Return output + # + # @return [AST::Node] + # + # @api private + # + def output + validate_node + processed_node + end + memoize :output + + private + + # Visit node + # + # @param [Node] node + # original untransformed node + # + # @return [Node] + # transformed node + # + # @api private + # + def visit(node) + preprocessor.call(node) + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + assert_children_amount(named_children.length) + end + + end # Emitter + + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor/emitter/anima.rb b/lib/morpher/compiler/preprocessor/emitter/anima.rb new file mode 100644 index 00000000..25eba861 --- /dev/null +++ b/lib/morpher/compiler/preprocessor/emitter/anima.rb @@ -0,0 +1,69 @@ +module Morpher + class Compiler + class Preprocessor + class Emitter + # Abstract base class for anima emitters + class Anima < self + include AbstractType + + children :model + + private + + # Return domain param + # + # @return [Transformer::Domain::Param] + # + # @api private + # + def param + Morpher::Evaluator::Transformer::Domain::Param.new( + model, + model.anima.attribute_names + ) + end + + class Dump < self + + register :anima_dump + + private + + # Return transformed node + # + # @param [Node] node + # + # @return [Node] + # + # @api private + # + def processed_node + s(:dump_attribute_hash, param) + end + + end # Dump + + class Load < self + + register :anima_load + + private + + # Return transformed node + # + # @param [Node] node + # + # @return [Node] + # + # @api private + # + def processed_node + s(:load_attribute_hash, param) + end + + end # Load + end # Anima + end # Emitter + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor/emitter/boolean.rb b/lib/morpher/compiler/preprocessor/emitter/boolean.rb new file mode 100644 index 00000000..2c5eec41 --- /dev/null +++ b/lib/morpher/compiler/preprocessor/emitter/boolean.rb @@ -0,0 +1,31 @@ +module Morpher + class Compiler + class Preprocessor + class Emitter + # Preprocessor for boolean primitive + class Boolean < self + register :boolean + + children + + NODE = s(:xor, s(:primitive, TrueClass), s(:primitive, FalseClass)) + + private + + # Return transformed node + # + # @param [Node] node + # + # @return [Node] + # + # @api private + # + def processed_node + NODE + end + + end # Boolean + end # Emitter + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor/emitter/key.rb b/lib/morpher/compiler/preprocessor/emitter/key.rb new file mode 100644 index 00000000..eacabad9 --- /dev/null +++ b/lib/morpher/compiler/preprocessor/emitter/key.rb @@ -0,0 +1,87 @@ +module Morpher + class Compiler + class Preprocessor + class Emitter + + # Namespace class for key preprocessors + class Key < self + include AbstractType + + # Key symbolization preprocessor + class Symbolize < self + + register :key_symbolize + + children :key, :operand + + private + + # Return transformed node + # + # @param [Node] node + # + # @return [Node] + # + # @api private + # + def processed_node + s(:key_transform, key.to_s, key.to_sym, operand) + end + + end # Symbolize + + # Neutral key preprocessor + class Neutral < self + + register :key + + children :key, :operand + + private + + # Return transformed node + # + # @param [Node] node + # + # @return [Node] + # + # @api private + # + def processed_node + s(:key_transform, key, key, operand) + end + + end # Neutral + + # Key transformation preprocessor + class Transform < self + register :key_transform + + children :from, :to, :operand + + private + + # Return transformed node + # + # @param [Node] node + # + # @return [Node] + # + # @api private + # + def processed_node + s( + :block, + s(:key_fetch, from), + visit(operand), + s(:key_dump, to) + ) + end + + end # Transform + + end # Key + end # Emitter + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor/emitter/noop.rb b/lib/morpher/compiler/preprocessor/emitter/noop.rb new file mode 100644 index 00000000..d225a328 --- /dev/null +++ b/lib/morpher/compiler/preprocessor/emitter/noop.rb @@ -0,0 +1,45 @@ +module Morpher + class Compiler + class Preprocessor + class Emitter + + # Noop emitter just descending into children + class Noop < self + + private + + # Return output + # + # @return [Node] + # + # @api private + # + def processed_node + mapped_children = node.children.map do |child| + if child.kind_of?(node.class) + visit(child) + else + child + end + end + s(node.type, *mapped_children) + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + end + + end # Noop + end # Emitter + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/compiler/preprocessor/emitter/param.rb b/lib/morpher/compiler/preprocessor/emitter/param.rb new file mode 100644 index 00000000..4f5dd84e --- /dev/null +++ b/lib/morpher/compiler/preprocessor/emitter/param.rb @@ -0,0 +1,50 @@ +module Morpher + class Compiler + class Preprocessor + class Emitter + + # Param domain transformer specific emitter + class Param < self + + register :param + + children :model + + private + + # Return output + # + # @return [Node] + # + # @api private + # + def processed_node + param = Morpher::Evaluator::Transformer::Domain::Param.new( + model, + remaining_children + ) + s(:raw, param) + end + + # Validate node + # + # @return [undefined] + # if successful + # + # @raise [Error] + # otherwise + # + # @api private + # + def validate_node + remaining_children.each_with_index do |child, index| + next if child.kind_of?(Symbol) + fail Error::ChildType, Symbol, child, index + end + end + + end # Noop + end # Emitter + end # Preprocessor + end # Compiler +end # Morpher diff --git a/lib/morpher/evaluation.rb b/lib/morpher/evaluation.rb new file mode 100644 index 00000000..fa034776 --- /dev/null +++ b/lib/morpher/evaluation.rb @@ -0,0 +1,118 @@ +module Morpher + # Abstract namespace class for evaluation states + class Evaluation + include AbstractType, Printer::Mixin, Adamantium::Flat, Anima.new( + :evaluator, + :input, + :output, + :success + ) + + private :success + + # Test if evaluation was successful + # + # @return [true] + # if evaluation was successful + # + # @return [false] + # otherwise + # + # @api private + # + alias_method :success?, :success + public :success? + + ERROR_DEFAULTS = IceNine.deep_freeze( + output: Undefined, + success: false + ) + + SUCCESS_DEFAULTS = IceNine.deep_freeze( + success: true + ) + + # Return error instance + # + # @param [Hash] attributes + # + # @return [Evaluation] + # + # @api private + # + def self.error(attributes) + new(ERROR_DEFAULTS.merge(attributes)) + end + + # Return successful instance + # + # @param [Hash] attributes + # + # @return [Evaluation] + # + # @api private + # + def self.success(attributes) + new(SUCCESS_DEFAULTS.merge(attributes)) + end + + # Evaluation state for nullary evaluators + class Nullary < self + + printer do + name + indent do + attributes :input, :output, :success? + visit :evaluator + end + end + + end + + # Evaluation state for nary evaluators + class Nary < self + include anima.add(:evaluations) + + printer do + name + indent do + attributes :input, :output, :success? + attribute_class :evaluator + visit_many :evaluations + end + end + + end # Evaluation + + # Evaluation state for unary evaluators + class Binary < self + include anima.add(:left_evaluation, :right_evaluation) + + printer do + name + indent do + attributes :input, :output, :success? + visit :left_evaluation + visit :right_evaluation + end + end + + end # Unary + + # Evaluation state for unary evaluators + class Unary < self + include anima.add(:operand_evaluation) + + printer do + name + indent do + attributes :input, :output, :success? + visit :operand_evaluation + visit :evaluator + end + end + + end # Unary + + end # Evaluation +end # Morpher diff --git a/lib/morpher/evaluator.rb b/lib/morpher/evaluator.rb new file mode 100644 index 00000000..afd6e8c2 --- /dev/null +++ b/lib/morpher/evaluator.rb @@ -0,0 +1,40 @@ +module Morpher + + # Abstract namespace class for non tracking evaluators + class Evaluator + include Adamantium::Flat, + Registry, + AbstractType, + Printer::Mixin, + NodeHelpers + + # Call evaluator in non tracking mode + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + abstract_method :call + + # Call evaluator in tracking mode + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + abstract_method :evaluation + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + abstract_method :inverse + + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/binary.rb b/lib/morpher/evaluator/binary.rb new file mode 100644 index 00000000..7e3eca9e --- /dev/null +++ b/lib/morpher/evaluator/binary.rb @@ -0,0 +1,46 @@ +module Morpher + + class Evaluator + + # Mixin for binary evaluators + module Binary + CONCORD = Concord::Public.new(:left, :right) + + PRINTER = lambda do |_| + name + indent do + visit(:left) + visit(:right) + end + end + + # Return node + # + # @return [AST::Node] + # + # @api private + # + def node + s(type, left.node, right.node) + end + + private + + # Hook called when module gets included + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.class_eval do + include CONCORD + printer(&PRINTER) + end + end + private_class_method :included + + end # Nary + + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/nary.rb b/lib/morpher/evaluator/nary.rb new file mode 100644 index 00000000..e0daf689 --- /dev/null +++ b/lib/morpher/evaluator/nary.rb @@ -0,0 +1,97 @@ +module Morpher + class Evaluator + + # Mixin for nary evaluators + module Nary + CONCORD = Concord::Public.new(:body) + + PRINTER = lambda do |_| + name + indent do + visit_many(:body) + end + end + + # Return AST + # + # @return [AST::Node] + # + # @api private + # + def node + s(type, *body.map(&:node)) + end + + private + + # Return positive evaluation + # + # @param [Object] input + # @param [Array] evaluations + # + # @return [Evaluation] + # + # @api private + # + def evaluation_positive(input, evaluations) + Evaluation::Nary.success( + evaluator: self, + input: input, + output: true, + evaluations: evaluations + ) + end + + # Return negative evaluation + # + # @param [Object] input + # @param [Array] evaluations + # + # @return [Evaluation] + # + # @api private + # + def evaluation_negative(input, evaluations) + Evaluation::Nary.success( + evaluator: self, + input: input, + output: false, + evaluations: evaluations + ) + end + + # Return evaluation error + # + # @param [Object] input + # @param [Array] evaluations + # + # @return [Evaluation] + # + # @api private + # + def evaluation_error(input, evaluations) + Evaluation::Nary.error( + evaluator: self, + input: input, + evaluations: evaluations + ) + end + + # Hook called when module gets included + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.class_eval do + include CONCORD + printer(&PRINTER) + end + end + private_class_method :included + + end # Nary + + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/nullary.rb b/lib/morpher/evaluator/nullary.rb new file mode 100644 index 00000000..38337d5e --- /dev/null +++ b/lib/morpher/evaluator/nullary.rb @@ -0,0 +1,92 @@ +module Morpher + class Evaluator + # Mixin to define nullary evaluators + module Nullary + + CONCORD = Concord::Public.new + + PRINTER = lambda do |_| + name + end + + # Instance methods for nullary evaluators + module InstanceMethods + + # Return default successful evaluation + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + def evaluation(input) + evaluation_success(input, call(input)) + end + + # Return node + # + # @return [AST::Node] + # + # @api private + # + def node + s(type) + end + + private + + # Return evaluation error for input + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + def evaluation_error(input) + Evaluation::Nullary.new( + evaluator: self, + input: input, + output: Undefined, + success: false + ) + end + + # Return evaluation success for input and output + # + # @param [Object] input + # @param [Object] output + # + # @return [Evaluation] + # + # @api private + # + def evaluation_success(input, output) + Evaluation::Nullary.new( + evaluator: self, + input: input, + output: output, + success: true + ) + end + + end # InstanceMethods + + # Hook called when module gets included + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.class_eval do + include InstanceMethods, CONCORD + printer(&PRINTER) + end + end + private_class_method :included + + end # Nullary + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/nullary/parameterized.rb b/lib/morpher/evaluator/nullary/parameterized.rb new file mode 100644 index 00000000..a8831906 --- /dev/null +++ b/lib/morpher/evaluator/nullary/parameterized.rb @@ -0,0 +1,48 @@ +module Morpher + class Evaluator + module Nullary + # Mixin to define parameterized nullary evaluators + module Parameterized + + CONCORD = Concord::Public.new(:param) + + PRINTER = lambda do |_| + name + indent do + attribute :param + end + end + + # Mixin for nullary parameterized evaluators + module InstanceMethods + + # Return node + # + # @return [AST::Node] + # + # @api private + # + def node + s(type, param) + end + + end # InstanceMethods + + # Hook called when module gets included + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.class_eval do + include InstanceMethods, Nullary::InstanceMethods, CONCORD + printer(&PRINTER) + end + end + private_class_method :included + + end # Nullary + end # Parameterized + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate.rb b/lib/morpher/evaluator/predicate.rb new file mode 100644 index 00000000..a02b28da --- /dev/null +++ b/lib/morpher/evaluator/predicate.rb @@ -0,0 +1,22 @@ +module Morpher + class Evaluator + # Abstract namespace class for predicate evaluators + class Predicate < self + include Transformer::Intransitive + + # Return inverse evaluator + # + # This is a very naive implementation. + # Subclasses can do a more elaborated choice. + # + # @return [Evaluator] + # + # @api private + # + def inverse + Negation.new(self) + end + + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate/boolean.rb b/lib/morpher/evaluator/predicate/boolean.rb new file mode 100644 index 00000000..978c2f1d --- /dev/null +++ b/lib/morpher/evaluator/predicate/boolean.rb @@ -0,0 +1,76 @@ +module Morpher + class Evaluator + class Predicate + # Evaluator for nary boolean predicates + class Boolean < self + include Nary + + # Call evaluator with input + # + # @param [Object] input + # + # @return [Boolean] + # + # @api private + # + def call(input) + body .public_send( + self.class::ENUMERABLE_METHOD + ) { |evaluator| evaluator.call(input) } + end + + # Return evaluation for input + # + # @param [Object] input + # + # @return [Evaluation::Nary] + # + # @api private + # + def evaluation(input) + klass = self.class + + evaluations = body.each_with_object([]) do |evaluator, aggregate| + evaluation = evaluator.evaluation(input) + aggregate << evaluation + next if evaluation.output.equal?(klass::OUTPUT_EXPECTATION) + return send(klass::ERROR_METHOD, input, aggregate) + end + + send(klass::SUCCESS_METHOD, input, evaluations) + end + + # Evaluator for nary and predicates + class And < self + register :and + + ENUMERABLE_METHOD = :all? + OUTPUT_EXPECTATION = true + ERROR_METHOD = :evaluation_negative + SUCCESS_METHOD = :evaluation_positive + end # And + + # Evaluator for nary or predicates + class Or < self + register :or + + ENUMERABLE_METHOD = :any? + OUTPUT_EXPECTATION = false + ERROR_METHOD = :evaluation_positive + SUCCESS_METHOD = :evaluation_negative + end # Or + + # Evaluator for nary xor predicates + class Xor < self + register :xor + + ENUMERABLE_METHOD = :one? + OUTPUT_EXPECTATION = false + ERROR_METHOD = :evaluation_positive + SUCCESS_METHOD = :evaluation_negative + end # Xor + + end # Boolean + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate/contradiction.rb b/lib/morpher/evaluator/predicate/contradiction.rb new file mode 100644 index 00000000..2479b771 --- /dev/null +++ b/lib/morpher/evaluator/predicate/contradiction.rb @@ -0,0 +1,36 @@ +module Morpher + class Evaluator + class Predicate + + # Evaluator for contradiction + class Contradiction < self + include Nullary + + register :false + + # Call predicate evaluator + # + # @param [Object] _input + # + # @return [false] + # + # @api private + # + def call(_input) + false + end + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + Tautology.new + end + + end # Contradiction + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate/eql.rb b/lib/morpher/evaluator/predicate/eql.rb new file mode 100644 index 00000000..d4f2998c --- /dev/null +++ b/lib/morpher/evaluator/predicate/eql.rb @@ -0,0 +1,50 @@ +module Morpher + class Evaluator + class Predicate + # Binary equal evaluator + class EQL < self + include Binary + + register :eql + + # Call evaluator + # + # @param [Object] input + # + # @return [true] + # if input is semantically equivalent to expectation + # + # @return [false] + # otherwise + # + # @api private + # + def call(input) + left.call(input).eql?(right.call(input)) + end + + # Return evaluation + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + def evaluation(input) + left_evaluation = left.evaluation(input) + right_evaluation = right.evaluation(input) + + Evaluation::Binary.success( + evaluator: self, + input: input, + output: left_evaluation.output.eql?(right_evaluation.output), + left_evaluation: left_evaluation, + right_evaluation: right_evaluation + ) + end + + end # EQL + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate/negation.rb b/lib/morpher/evaluator/predicate/negation.rb new file mode 100644 index 00000000..763ce1c5 --- /dev/null +++ b/lib/morpher/evaluator/predicate/negation.rb @@ -0,0 +1,52 @@ +module Morpher + class Evaluator + class Predicate + # Predicate negation + class Negation < self + include Unary + + register :negate + + # Return evaluation for input + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + def evaluation(input) + operand_output = operand.call(input) + evaluation_success(input, operand_output, !operand_output) + end + + # Call evaluator + # + # @param [Object] input + # + # @return [true] + # if input NOT evaluated to true under operand + # + # @return [false] + # otherwise + # + # @api private + # + def call(input) + !operand.call(input) + end + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + operand + end + + end # Negation + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate/primitive.rb b/lib/morpher/evaluator/predicate/primitive.rb new file mode 100644 index 00000000..5f90761b --- /dev/null +++ b/lib/morpher/evaluator/predicate/primitive.rb @@ -0,0 +1,49 @@ +module Morpher + class Evaluator + class Predicate + # Abstract namespace class for predicate evaluators on primitives + class Primitive < self + include Nullary::Parameterized + + # Evaluator for exact primitive match + class Exact < self + + register :primitive + + # Call evaluator + # + # @param [Object] object + # + # @return [true] + # if object's type is #equal? + # + # @api private + # + def call(object) + object.class.equal?(param) + end + + end # Exact + + # Evaluator for permissive primtivie match + class Permissive < self + register :is_a + + # Call evaluator + # + # @param [Object] object + # + # @return [true] + # if objects type equals exactly + # + # @api private + # + def call(object) + object.kind_of?(param) + end + + end # Permissive + end # Primitive + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/predicate/tautology.rb b/lib/morpher/evaluator/predicate/tautology.rb new file mode 100644 index 00000000..e35adc31 --- /dev/null +++ b/lib/morpher/evaluator/predicate/tautology.rb @@ -0,0 +1,36 @@ +module Morpher + class Evaluator + class Predicate + + # Evaluator for trautology + class Tautology < self + include Nullary + + register :true + + # Call predicate evaluator + # + # @param [Object] _input + # + # @return [true] + # + # @api private + # + def call(_input) + true + end + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + Contradiction.new + end + + end # Tautology + end # Predicate + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer.rb b/lib/morpher/evaluator/transformer.rb new file mode 100644 index 00000000..996c6d0d --- /dev/null +++ b/lib/morpher/evaluator/transformer.rb @@ -0,0 +1,75 @@ +module Morpher + class Evaluator + # Abstract namespace class for transforming evaluators + class Transformer < self + include AbstractType + + # Error raised when transformation cannot continue + class TransformError < RuntimeError + include Concord.new(:transformer, :input) + end + + # Test evaluator transformer is transitive + # + # A transitive evaluator allows to inverse an operation + # via its #inverse evaluator. + # + # @return [true] + # if transformer is transitive + # + # @return [false] + # otherwise + # + # @api private + # + abstract_method :transitive? + + # Mixin for evaluators that are transitive by definition + module Transitive + + # Test if evaluator is transitive + # + # @return [false] + # + # @api private + # + def transitive? + true + end + + end # Intransitive + + # Mixin for evaluators that are intransitive by definition + module Intransitive + + # Test if evaluator is transitive + # + # @return [false] + # + # @api private + # + def transitive? + false + end + + end # Intransitive + + private + + # Raise transform error + # + # @param [Object] input + # + # @raise [TransformError] + # + # @return [undefined] + # + # @api private + # + def raise_transform_error(input) + fail TransformError.new(self, input) + end + + end # Transform + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/attribute.rb b/lib/morpher/evaluator/transformer/attribute.rb new file mode 100644 index 00000000..c544d0fd --- /dev/null +++ b/lib/morpher/evaluator/transformer/attribute.rb @@ -0,0 +1,25 @@ +module Morpher + class Evaluator + class Transformer + # Transformer to return a specific attribute of input + class Attribute < self + include Nullary::Parameterized, Intransitive + + register :attribute + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + input.public_send(param) + end + + end # Attribute + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/block.rb b/lib/morpher/evaluator/transformer/block.rb new file mode 100644 index 00000000..04f3d682 --- /dev/null +++ b/lib/morpher/evaluator/transformer/block.rb @@ -0,0 +1,81 @@ +module Morpher + class Evaluator + class Transformer + + # Evaluator to perform n transformations in a row + class Block < self + include Nary + + register :block + + # Test if evaluator is transitive + # + # @return [true] + # if block is transitive + # + # @return [false] + # otherwise + # + # @api private + # + def transitive? + body.all?(&:transitive?) + end + + # Call transformer + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + body.reduce(input) do |state, evaluator| + evaluator.call(state) + end + end + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + self.class.new(body.reverse.map(&:inverse)) + end + + # Return evaluation for input + # + # @param [Object] input + # + # @return [Evaluation::Nary] + # + # @api private + # + # rubocop:disable MethodLength + # + def evaluation(input) + state = input + + evaluations = body.each_with_object([]) do |evaluator, aggregate| + evaluation = evaluator.evaluation(state) + aggregate << evaluation + unless evaluation.success? + return evaluation_error(input, aggregate) + end + state = evaluation.output + end + + Evaluation::Nary.success( + evaluator: self, + input: input, + output: state, + evaluations: evaluations + ) + end + end # Block + end # Transformer + end # Evaluato +end # Morpher diff --git a/lib/morpher/evaluator/transformer/coerce.rb b/lib/morpher/evaluator/transformer/coerce.rb new file mode 100644 index 00000000..02f126fc --- /dev/null +++ b/lib/morpher/evaluator/transformer/coerce.rb @@ -0,0 +1,166 @@ +module Morpher + class Evaluator + class Transformer + # Abstract namespace class for coercing transformers + class Coerce < self + include AbstractType, Nullary::Parameterized, Transitive + + # Parse mixin for cases the parsing possibly results + # in Argument and Type errors. + module Parse + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + invoke(input) + rescue ArgumentError, TypeError + raise_transform_error(input) + end + + # Return evaluation + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + def evaluation(input) + evaluation_success(input, invoke(input)) + rescue ArgumentError, TypeError + evaluation_error(input) + end + end # Parse + + # Evaluator for parsing an integer + class ParseInt < self + + include Parse + + register :parse_int + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + IntToString.new(param) + end + + private + + # Invoke coercion + # + # @return [Integer] + # + # @raise [ArgumentError, TypeError] + # if coercion does not succeed + # + # @api private + # + def invoke(input) + Integer(input, param) + end + + end # ParseInt + + # Evaluator for dumping fixnums to strings + class IntToString < self + + register :int_to_string + + # Call evaluator + # + # @param [Object] input + # + # @return [Hash] + # + # @api private + # + def call(input) + input.to_s(param) + end + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + ParseInt.new(param) + end + + end # IntToString + + # Evaluator for parsing an ISO8601 String into a DateTime + class ParseIso8601DateTime < self + + include Parse + + register :parse_iso8601_date_time + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + DateTimeToIso8601String.new(param) + end + + private + + # Invoke coercion + # + # @return [DateTime] + # + # @raise [ArgumentError, TypeError] + # if coercion does not succeed + # + # @api private + # + def invoke(input) + DateTime.iso8601(input) + end + end # ParseIso8601DateTime + + # Evaluator for dumping a DateTime to an ISO8601 string + class DateTimeToIso8601String < self + register :date_time_to_iso8601_string + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + input.iso8601(param) + end + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + ParseIso8601DateTime.new(param) + end + end # DateTimeToIso8601String + end # Fixnum + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/custom.rb b/lib/morpher/evaluator/transformer/custom.rb new file mode 100644 index 00000000..468a0eab --- /dev/null +++ b/lib/morpher/evaluator/transformer/custom.rb @@ -0,0 +1,34 @@ +module Morpher + class Evaluator + class Transformer < self + # Custom transformer with external injected behavior + class Custom < self + include Nullary::Parameterized, Transitive + register :custom + + # Call transformer with input + # + # @param [Object] + # + # @return [undefinedo] + # + # @api private + # + def call(input) + param.first.call(input) + end + + # Return inverse transformer + # + # @return [Evaluator::Transformer] + # + # @api private + # + def inverse + self.class.new(param.reverse) + end + + end # Custom + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/domain.rb b/lib/morpher/evaluator/transformer/domain.rb new file mode 100644 index 00000000..72db373d --- /dev/null +++ b/lib/morpher/evaluator/transformer/domain.rb @@ -0,0 +1,86 @@ +module Morpher + class Evaluator + class Transformer + # Abstract namespace class for transformers from/to domain + class Domain < self + include AbstractType, Nullary::Parameterized, Transitive + + private + + # Call block with param attributes + # + # @param [Object] aggregate + # + # @return [Object] + # + # @api private + # + def transform(aggregate, &block) + param.attributes.each_with_object(aggregate, &block) + end + + # Mixin for domain dumpers + module Dump + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + self.class::Load.new(param) + end + + private + + # Dump object + # + # @param [Symbol] left + # @param [Symbol] right + # + # @return [Object] + # + # @api private + # + def dump(&block) + transform({}, &block) + end + + end # Dump + + # Mixin for domain loaders + module Load + include AbstractType + + # Return inverse evaluator + # + # @return [Evaluator] + # + # @api private + # + def inverse + self.class::Dump.new(param) + end + + private + + # Load object + # + # @param [Symbol] left + # @param [Symbol] right + # + # @return [Object] + # + # @api private + # + def load(&block) + transform(param.model.allocate, &block) + end + + end # Load + + end # Domain + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/domain/attribute_accessors.rb b/lib/morpher/evaluator/transformer/domain/attribute_accessors.rb new file mode 100644 index 00000000..bb8ac982 --- /dev/null +++ b/lib/morpher/evaluator/transformer/domain/attribute_accessors.rb @@ -0,0 +1,60 @@ +module Morpher + class Evaluator + class Transformer + class Domain + # Abstract namespace class for domain objects via attribute accessors + class AttributeAccessors < self + include AbstractType + + # Evaluator for dumping domain objects via instance variables + class Dump < self + include Domain::Dump + + register :dump_attribute_accessors + + # Call evaluator + # + # @param [Object] input + # + # @return [Hash] + # + # @api private + # + def call(input) + dump do |attribute, attributes| + name = attribute.name + attributes[name] = input.public_send(name) + end + end + + end # Dump + + # Evaluator for loading domain objects via attributes hash + class Load < self + include Domain::Load + + register :load_attribute_accessors + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + load do |attribute, object| + object.public_send( + attribute.writer, + input.fetch(attribute.name) + ) + end + end + + end # Load + end # Domain + end # Anima + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/domain/attribute_hash.rb b/lib/morpher/evaluator/transformer/domain/attribute_hash.rb new file mode 100644 index 00000000..a3ba2ba6 --- /dev/null +++ b/lib/morpher/evaluator/transformer/domain/attribute_hash.rb @@ -0,0 +1,52 @@ +module Morpher + class Evaluator + class Transformer + class Domain < self + # Abstract namespace class for domain objects on attributes hash + class AttributeHash < self + include AbstractType + + # Evaluator for dumping domain objects via attributes hash + class Dump < self + include Domain::Dump + + register :dump_attribute_hash + + # Call evaluator + # + # @param [Object] input + # + # @return [Hash] + # + # @api private + # + def call(input) + input.to_h + end + + end # Dump + + # Evaluator for loading domain objects via attributes hash + class Load < self + include Domain::Load + + register :load_attribute_hash + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + param.model.new(input) + end + + end # Load + end # Domain + end # Anima + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/domain/instance_variables.rb b/lib/morpher/evaluator/transformer/domain/instance_variables.rb new file mode 100644 index 00000000..57e23fc6 --- /dev/null +++ b/lib/morpher/evaluator/transformer/domain/instance_variables.rb @@ -0,0 +1,60 @@ +module Morpher + class Evaluator + class Transformer + class Domain < self + # Abstract namespace class for domain objects via instance variables + class InstanceVariables < self + include AbstractType + + # Evaluator for dumping domain objects via instance variables + class Dump < self + include Domain::Dump + + register :dump_instance_variables + + # Call evaluator + # + # @param [Object] input + # + # @return [Hash] + # + # @api private + # + def call(input) + dump do |attribute, attributes| + attributes[attribute.name] = + input.instance_variable_get(attribute.ivar_name) + end + end + + end # Dump + + # Evaluator for loading domain objects via instance variables + class Load < self + include Domain::Load + + register :load_instance_variables + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + load do |attribute, object| + object.instance_variable_set( + attribute.ivar_name, + input.fetch(attribute.name) + ) + end + end + + end # Load + end # Domain + end # Anima + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/domain/param.rb b/lib/morpher/evaluator/transformer/domain/param.rb new file mode 100644 index 00000000..a94c05fb --- /dev/null +++ b/lib/morpher/evaluator/transformer/domain/param.rb @@ -0,0 +1,54 @@ +module Morpher + class Evaluator + class Transformer + class Domain < self + + # Domain specific transformer parameter + class Param + include Adamantium, Concord::Public.new(:model, :attribute_names) + + # Return attributes + # + # @return [Enumerable] + # + # @api private + # + def attributes + attribute_names.map(&Attribute.method(:new)) + end + memoize :attributes + + # Attribute on a domain transformer param + class Attribute + include Adamantium, Concord::Public.new(:name) + + # Return instance variable name + # + # @return [Symbol] + # + # @api private + # + def ivar_name + :"@#{name}" + end + memoize :ivar_name + + # Return writer name + # + # @return [Symbol] + # + # @api private + # + def writer + :"#{name}=" + end + memoize :writer + + end # Attribute + + end # Param + + end # Domain + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/guard.rb b/lib/morpher/evaluator/transformer/guard.rb new file mode 100644 index 00000000..fca2d2fc --- /dev/null +++ b/lib/morpher/evaluator/transformer/guard.rb @@ -0,0 +1,62 @@ +module Morpher + class Evaluator + class Transformer + + # Transformer that allows to guard transformation process + # with a predicate on input + class Guard < self + include Unary, Transitive + + register :guard + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # if input evaluates true under predicate + # + # @raise [TransformError] + # otherwise + # + # @api private + # + def call(input) + if operand.call(input) + input + else + fail TransformError.new(self, input) + end + end + + # Return evaluation + # + # @param [Object] input + # + # @return [Evaluation::Guard] + # + # @api private + # + def evaluation(input) + operand_evaluation = operand.evaluation(input) + if operand_evaluation.output + evaluation_success(input, operand_evaluation, input) + else + evaluation_error(input, operand_evaluation) + end + end + + # Return inverse evaluator + # + # @return [self] + # + # @api private + # + def inverse + self + end + + end # Guard + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/hash_transform.rb b/lib/morpher/evaluator/transformer/hash_transform.rb new file mode 100644 index 00000000..d1c576d9 --- /dev/null +++ b/lib/morpher/evaluator/transformer/hash_transform.rb @@ -0,0 +1,149 @@ +module Morpher + class Evaluator + class Transformer + + # Too complex hash transformation evaluator + # + # FIXME: Should be broken up in better + # primitives a decompose, compose pair + # + # @api private + # + class HashTransform < self + include Nary + + register :hash_transform + + # Test if evaluator is transitive + # + # FIXME: Needs to be calculated dynamically + # + # @return [true] + # if evaluator is transitive + # + # @return [false] + # otherwise + # + # @api private + # + def transitive? + body.all?(&self.class.method(:transitive_keypair?)) + end + + # Test if evaluator is a keypair + # + # FIXME: Refactor the need for this away. + # + # This is a side effect from this class is + # generally to big in sense of SRP. + # Must be refactorable away. But dunno now. + # Still exploring. + # + # @param [Evaluator] + # + # @api private + # + def self.transitive_keypair?(evaluator) + return false unless evaluator.kind_of?(Block) + + body = evaluator.body + + left, operator, right = body if body.length.equal?(3) + + left.kind_of?(Key::Fetch) && + right.kind_of?(Key::Dump) && + operator.transitive? + end + + # Call evaluator + # + # @param [Object] input + # + # @return [Object] + # + # @api private + # + def call(input) + content = body.map do |node| + node.call(input) + end + Hash[content] + end + + # Return inverse evaluator + # + # @return [HashTransform] + # + # @api private + # + def inverse + self.class.new(body.map(&:inverse)) + end + + # Return evaluation + # + # @param [Input] + # + # @return [Evaluation::Nary] + # + # @api private + # + # rubocop:disable MethodLength + # + def evaluation(input) + evaluations = body.each_with_object([]) do |evaluator, aggregate| + evaluation = evaluator.evaluation(input) + aggregate << evaluation + unless evaluation.success? + return evaluation_error(input, aggregate) + end + end + + output = Hash[evaluations.map(&:output)] + + Evaluation::Nary.success( + evaluator: self, + input: input, + evaluations: evaluations, + output: output + ) + end + + private + + # Return evaluation error + # + # @return [Object] input + # + # @return [Array] evaluations + # + # @api private + # + def evaluation_error(input, evaluations) + Evaluation::Nary.error( + evaluator: self, + input: input, + evaluations: evaluations + ) + end + + # Build evaluator from node + # + # @param [Compiler] compiler + # @param [Node] node + # + # @return [Evaluator] + # + # @api private + # + def self.build(compiler, node) + body = node.children.map do |child| + compiler.call(child) + end + new(body) + end + + end # HashTransform + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/input.rb b/lib/morpher/evaluator/transformer/input.rb new file mode 100644 index 00000000..283696e4 --- /dev/null +++ b/lib/morpher/evaluator/transformer/input.rb @@ -0,0 +1,37 @@ +module Morpher + class Evaluator + class Transformer + + # Identity transformer which always returns +input+ + class Input < self + include Nullary, Transitive + + register :input + + # Call evaluator with input + # + # @param [Object] input + # + # @return [Object] + # always returns input + # + # @api private + # + def call(input) + input + end + + # Return inverse evaluator + # + # @return [Evaluator::Transformer] + # + # @api private + # + def inverse + self + end + + end # Input + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/key.rb b/lib/morpher/evaluator/transformer/key.rb new file mode 100644 index 00000000..0d30e16b --- /dev/null +++ b/lib/morpher/evaluator/transformer/key.rb @@ -0,0 +1,86 @@ +module Morpher + class Evaluator + class Transformer + # Abstract namespace class for evaluators operating on hash keys + class Key < self + include AbstractType, Nullary::Parameterized, Intransitive + + # Evaluator for dumping hash keys + class Dump < self + + register :key_dump + + # Call evaluator + # + # @param [Object] object + # + # @return [Array] + # + # @api private + # + def call(object) + [param, object] + end + + # Return inverse evaluator + # + # @return [Fetch] + # + # @api private + # + def inverse + Fetch.new(param) + end + + end # Dump + + # Evaluator to fetch a specific hash key + class Fetch < self + + register :key_fetch + + # Call evaluator + # + # @param [Hash] object + # + # @return [Object] + # + # @api private + # + def call(object) + object.fetch(param) do + fail TransformError.new(self, object) + end + end + + # Return evaluation + # + # @param [Object] input + # + # @return [Evaluation] + # + # @api private + # + def evaluation(input) + output = input.fetch(param) do + return evaluation_error(input) + end + + evaluation_success(input, output) + end + + # Return inverse evaluator + # + # @return [Dump] + # + # @api private + # + def inverse + Dump.new(param) + end + + end # Fetch + end # Key + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/map.rb b/lib/morpher/evaluator/transformer/map.rb new file mode 100644 index 00000000..559906d8 --- /dev/null +++ b/lib/morpher/evaluator/transformer/map.rb @@ -0,0 +1,100 @@ +module Morpher + class Evaluator + class Transformer + + # Transformer over each element in an enumerable + class Map < self + include Unary + + register :map + + # Test if evaluator is transitive + # + # @return [true] + # if evaluator is transitive + # + # @return [false] + # otherwise + # + # @api private + # + def transitive? + operand.transitive? + end + + # Call evaluator + # + # @param [Enumerable#map] input + # + # @return [Enumerable] + # if input evaluates true under predicate + # + # @raise [TransformError] + # otherwise + # + # @api private + # + def call(input) + input.map(&operand.method(:call)) + end + + # Return evaluation + # + # @param [Enumerable#map] input + # + # @return [Evaluation] + # + # @api private + # + # rubocop:disable MethodLength + # + def evaluation(input) + evaluations = input.each_with_object([]) do |item, aggregate| + evaluation = operand.evaluation(item) + aggregate << evaluation + unless evaluation.success? + return evaluation_error(input, aggregate) + end + end + + Evaluation::Nary.success( + evaluator: self, + input: input, + output: evaluations.map(&:output), + evaluations: evaluations + ) + end + + # Return inverse evaluator + # + # @return [Evaluator::Transformer] + # + # @api private + # + def inverse + self.class.new(operand.inverse) + end + + private + + # Return evaluation error + # + # @param [Object] input + # @param [Array] evaluations + # + # @return [Evaluation] + # + # @api private + # + def evaluation_error(input, evaluations) + Evaluation::Nary.error( + evaluator: self, + input: input, + evaluations: evaluations + ) + end + + end # Guard + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/merge.rb b/lib/morpher/evaluator/transformer/merge.rb new file mode 100644 index 00000000..f2c9a00b --- /dev/null +++ b/lib/morpher/evaluator/transformer/merge.rb @@ -0,0 +1,25 @@ +module Morpher + class Evaluator + class Transformer + # Transformer to merge input into defaults + class Merge < self + include Intransitive, Nullary::Parameterized + + register :merge + + # Call evaluator for input + # + # @param [Object] input + # + # @return [Object] output + # + # @api private + # + def call(input) + param.merge(input) + end + + end # Merge + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/transformer/static.rb b/lib/morpher/evaluator/transformer/static.rb new file mode 100644 index 00000000..5a98cc7d --- /dev/null +++ b/lib/morpher/evaluator/transformer/static.rb @@ -0,0 +1,27 @@ +module Morpher + class Evaluator + class Transformer + + # Transformer that always returns the passed +param+ + class Static < self + include Nullary::Parameterized, Intransitive + + register :static + + # Call evaluator with input + # + # @param [Object] _input + # + # @return [Object] + # alwasys returns the param + # + # @api private + # + def call(_input) + param + end + + end # Static + end # Transformer + end # Evaluator +end # Morpher diff --git a/lib/morpher/evaluator/unary.rb b/lib/morpher/evaluator/unary.rb new file mode 100644 index 00000000..6e2151f6 --- /dev/null +++ b/lib/morpher/evaluator/unary.rb @@ -0,0 +1,79 @@ +module Morpher + class Evaluator + + # Mixin for unary evaluators + module Unary + CONCORD = Concord::Public.new(:operand) + + PRINTER = lambda do |_| + name + indent do + visit(:operand) + end + end + + # Return node + # + # @return [AST::Node] + # + # @api private + # + def node + s(type, operand.node) + end + + private + + # Return success evaluation for input + # + # @param [Object] input + # + # @return [Evalation::Unary] + # + # @api private + # + def evaluation_success(input, operand_evaluation, output) + Evaluation::Unary.success( + evaluator: self, + input: input, + operand_evaluation: operand_evaluation, + output: output + ) + end + + # Return error evaluation for input + # + # @param [Object] input + # + # @return [Evalation::Unary] + # + # @api private + # + def evaluation_error(input, operand_evaluation) + Evaluation::Unary.error( + evaluator: self, + input: input, + operand_evaluation: operand_evaluation + ) + end + + private + + # Hook called when module gets included + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.class_eval do + include CONCORD + printer(&PRINTER) + end + end + private_class_method :included + + end # Unary + + end # Evaluator +end # Morpher diff --git a/lib/morpher/node_helpers.rb b/lib/morpher/node_helpers.rb new file mode 100644 index 00000000..813bf6d7 --- /dev/null +++ b/lib/morpher/node_helpers.rb @@ -0,0 +1,19 @@ +module Morpher + # Node helpers + module NodeHelpers + + # Build node + # + # @param [Symbol] type + # + # @return [Parser::AST::Node] + # + # @api private + # + def s(type, *children) + AST::Node.new(type, children) + end + module_function :s + + end # NodeHelpers +end # Morpher diff --git a/lib/morpher/printer.rb b/lib/morpher/printer.rb new file mode 100644 index 00000000..e188cc59 --- /dev/null +++ b/lib/morpher/printer.rb @@ -0,0 +1,233 @@ +module Morpher + + # Evaluation and Evaluator pretty printer + class Printer + include Adamantium::Flat, Concord.new(:object, :output, :indent_level) + + INDENT = ' '.freeze + + REGISTRY = {} + + # Run pretty printer on object + # + # @param [Object] object + # the object to be pretty printed + # @param [IO] output + # the output to write to + # @param [Fixnum] indent_level + # the current indentation level + # + # @return [self] + # + # @api private + # + def self.run(object, output, indent_level = 0) + printer = new(object, output, indent_level) + block = lookup(object) + printer.instance_eval(&block) + end + + # Perform type lookup + # + # FIXME: Instanciate type lookup once and allow caching. + # + # @param [Evaluation, Evaluator] object + # + # @return [Proc] + # if found + # + # @raise [PrinterMissingException] + # otherwise + # + # @api private + # + def self.lookup(object) + TypeLookup.new(REGISTRY).call(object) + end + + private + + # Visit a child + # + # @param [Node] child + # + # @api private + # + # @return [undefined] + # + def visit_child(child) + self.class.run(child, output, indent_level.succ) + end + + # Visit a child by name + # + # @param [Symbol] name + # the attribute name of the child to visit + # + # @return [undefined] + # + # @api private + # + def visit(name) + child = object.public_send(name) + child_label(name) + visit_child(child) + end + + # Visit many children + # + # @param [Symbol] name + # the name of the collection attribute with children to visit + # + # @return [undefined] + # + # @api private + # + def visit_many(name) + children = object.public_send(name) + child_label(name) + children.each do |child| + visit_child(child) + end + end + + # Print attribute class + # + # @param [Symbol] name + # + # @return [undefined] + # + # @api private + # + def attribute_class(name) + label_value(name, object.public_send(name).class) + end + + # Print inspected attribute value with label + # + # @return [undefined] + # + # @api private + # + def attribute(name) + label_value(name, object.public_send(name)) + end + + # Print attributes of object + # + # @return [undefined] + # + # @api private + # + def attributes(*names) + names.each do |name| + attribute(name) + end + end + + # Print name of object + # + # @return [undefined] + # + # @api private + # + def name + puts(object.class.name) + end + + # Return string indented with current level + # + # @param [String] content + # + # @return [String] + # + # @api private + # + def indented(content) + "#{indentation_prefix}#{content}" + end + + # Return indentation prefix + # + # @return [String] + # + # @api private + # + def indentation_prefix + INDENT * indent_level + end + memoize :indentation_prefix + + # Add content to output at current indentation and close line + # + # @param [String] content + # + # @return [undefined] + # + # @api private + # + def puts(string) + output.puts(indented(string)) + end + + # Write content to output at current indentation + # + # @param [String] content + # + # @return [undefined] + # + # @api private + # + def write(string) + output.write(indented(string)) + end + + # Write child label to output at current indentation + # + # @param [String] label + # + # @return [undefined] + # + # @api private + # + def child_label(string) + puts("#{string}:") + end + + # Call block inside indented context + # + # @return [undefined] + # + # @api private + # + def indent(&block) + printer = new(object, output, indent_level.succ) + printer.instance_eval(&block) + end + + # Return new printer + # + # @return [Printer] + # + # @api private + # + def new(*arguments) + self.class.new(*arguments) + end + + # Print label with value + # + # @param [String] label + # @param [Object] value + # + # @return [undefined] + # + # @api private + # + def label_value(label, value) + write("#{label}: ") + output.puts(value.inspect) + end + + end # Printer +end # Morpher diff --git a/lib/morpher/printer/mixin.rb b/lib/morpher/printer/mixin.rb new file mode 100644 index 00000000..14866e2a --- /dev/null +++ b/lib/morpher/printer/mixin.rb @@ -0,0 +1,58 @@ +module Morpher + class Printer + + # Printer behavior mixin + module Mixin + + # Class level methods to be mixed in + module ClassMethods + + # Register printer block for class + # + # @return [self] + # + # @api private + # + def printer(&block) + REGISTRY[self] = block + end + + end # ClassMethods + + # Instance level methods to be mixed in + module InstanceMethods + + # Return description + # + # @return [String] + # + # @api private + # + def description + io = StringIO.new + Printer.run(self, io) + io.rewind + io.read + end + + end # InstanceMethods + + # Callback whem module gets included + # + # @param [Class, Module] descendant + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.class_eval do + extend ClassMethods + include InstanceMethods + end + end + private_class_method :included + + end # Mixin + end # Printer +end # Morpher diff --git a/lib/morpher/registry.rb b/lib/morpher/registry.rb new file mode 100644 index 00000000..a0abe4b1 --- /dev/null +++ b/lib/morpher/registry.rb @@ -0,0 +1,51 @@ +module Morpher + # Mixin for to provide a node registry + module Registry + + # Hook called when module is included + # + # @param [Module, Class] descendant + # + # @return [undefined] + # + # @api private + # + def self.included(descendant) + descendant.const_set(:REGISTRY, {}) + descendant.class_eval do + extend ClassMethods + end + end + + # Return node type + # + # @return [Symbol] + # + # @api private + # + def type + self.class::TYPE + end + + # Methods to get mixed in at singleton level + module ClassMethods + + # Register evaluator under name + # + # TODO: Disallow duplicate registration under same name + # + # @param [Symbol] name + # + # @return [undefined] + # + # @api private + # + def register(name) + const_set(:TYPE, name) + self::REGISTRY[name] = self + end + + end # ClassMethods + + end # Registry +end # Morpher diff --git a/lib/morpher/type_lookup.rb b/lib/morpher/type_lookup.rb new file mode 100644 index 00000000..7cf1a689 --- /dev/null +++ b/lib/morpher/type_lookup.rb @@ -0,0 +1,51 @@ +module Morpher + + # Type lookup via registry and superclass chaining + # + # TODO: Cache results. + # + class TypeLookup + include Adamantium::Flat, Concord.new(:registry) + + # Error raised on compiling unknown nodes + class TypeNotFoundError < RuntimeError + include Concord.new(:type) + + # Return exception error message + # + # @return [String] + # + # @api private + # + def message + "Node type: #{type.inspect} is unknown" + end + + end # TypeNotFoundError + + # Perform type lookup + # + # @param [Object] object + # + # @return [Object] + # if found + # + # @raise [TypeNotFoundError] + # otherwise + # + # @api private + # + def call(object) + current = target = object.class + while current != Object + if registry.key?(current) + return registry.fetch(current) + end + current = current.superclass + end + + fail TypeNotFoundError, target + end + + end # TypeLookup +end # Morpher diff --git a/mutant.gemspec b/mutant.gemspec index f01dc83b..b9d6a139 100644 --- a/mutant.gemspec +++ b/mutant.gemspec @@ -30,7 +30,6 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency('equalizer', '~> 0.0.9') gem.add_runtime_dependency('ice_nine', '~> 0.11.1') gem.add_runtime_dependency('memoizable', '~> 0.4.2') - gem.add_runtime_dependency('morpher', '~> 0.3.1') gem.add_runtime_dependency('parser', '~> 2.7.2') gem.add_runtime_dependency('procto', '~> 0.0.2') gem.add_runtime_dependency('regexp_parser', '~> 1.8.2')