Permalink
Browse files

implemented function nodes, and improved node sharing

  • Loading branch information...
1 parent ebc2a35 commit c0172442db781ada9b9e23b0535108e479dc1cbd @jkutner jkutner committed Jun 3, 2011
Showing with 339 additions and 15 deletions.
  1. +26 −2 lib/core/atoms.rb
  2. +44 −9 lib/core/nodes.rb
  3. +15 −2 lib/dsl/ferrari.rb
  4. +17 −1 lib/rule_helper.rb
  5. +181 −0 spec/function_spec.rb
  6. +0 −1 spec/hello_spec.rb
  7. +56 −0 spec/node_sharing_spec.rb
View
@@ -45,8 +45,32 @@ def shareable?(atom)
@deftemplate == atom.deftemplate &&
@proc == atom.proc
end
- end
-
+ end
+
+ class FunctionAtom < Atom
+
+ attr_reader :arguments
+
+ def initialize(tag, template, arguments, block)
+ @tag = tag
+ @method = nil
+ @deftemplate = template
+ @arguments = arguments
+ @proc = block
+ end
+
+ def shareable?(atom)
+ FunctionAtom === atom &&
+ @deftemplate == atom.deftemplate &&
+ @arguments == atom.arguments &&
+ @proc == atom.proc
+ end
+
+ def to_s
+ return "#{self.class},#{@deftemplate},#{@arguments.inspect}"
+ end
+ end
+
# TODO use this
class BlockAtom < PropertyAtom
def shareable?(atom)
View
@@ -213,7 +213,13 @@ def create_bridge_node(pattern)
end
def create_property_node(atom)
- node = atom.kind_of?(EqualsAtom) ? EqualsNode.new(@bucket, atom) : PropertyNode.new(@bucket, atom)
+ if atom.kind_of?(EqualsAtom)
+ node = EqualsNode.new(@bucket, atom)
+ elsif atom.kind_of?(FunctionAtom)
+ node = FunctionNode.new(@bucket, atom)
+ else
+ node = PropertyNode.new(@bucket, atom)
+ end
@atom_nodes.each {|n| return n if n.shareable? node}
@atom_nodes.push node
return node
@@ -401,6 +407,9 @@ def retract(assertable)
def assert(assertable)
k = assertable.fact.object.send(@atom.method)
+
+ # TODOwe need to do this for ALL tags if this node is shared
+ assertable.add_tag(@atom.tag, k)
propagate_assert assertable, (@values[k] ? @values[k] : {})
rescue NoMethodError => e
@bucket.add_error Error.new(:no_method, :warn, {
@@ -424,6 +433,7 @@ def retract(fact)
def assert(fact)
k = fact.object.send(@atom.method)
+ # TODO we should create the Assertion object here, not in propogate
propagate_assert fact, (@values[k] ? @values[k] : {})
rescue NoMethodError => e
@bucket.add_error Error.new(:no_method, :warn, {
@@ -459,6 +469,7 @@ class PropertyNode < AtomNode
def assert(assertable)
begin
val = assertable.fact.object.send(@atom.method)
+ assertable.add_tag(@atom.tag, val)
rescue NoMethodError => e
@bucket.add_error Error.new(:no_method, :warn, {
:object => assertable.fact.object.to_s,
@@ -489,6 +500,22 @@ def hash_by(atom)
end
end
+ # This node class is used for matching properties of a fact.
+ class FunctionNode < AtomNode
+ def assert(assertable)
+ begin
+ super if @atom.proc.call(assertable.fact.object, *@atom.arguments)
+ rescue Exception => e
+ @bucket.add_error Error.new(:proc_call, :error, {
+ :object => fact.object.to_s,
+ :function => true,
+ :arguments => @atom.arguments,
+ :message => e.message
+ })
+ end
+ end
+ end
+
# This node class is used to match properties of one with the properties
# of any other already matched fact. It differs from the other AtomNodes
# because it does not perform any inline matching. The match method is only
@@ -559,21 +586,22 @@ def retract(assertable)
end
class BridgeNode < BaseBridgeNode
- def propagate_assert(fact)
- # create the partial match
- mr = MatchResult.new
- mr.is_match = true
+ def assert(assertable)
+ fact = assertable.fact
+ mr = MatchResult.new(assertable.tags)
+ mr.is_match = true
mr.recency.push fact.recency
@pattern.atoms.each do |atom|
mr.fact_hash[atom.tag] = fact.id
if atom == @pattern.head
- # HACK its a pain to have to check for this, can we make it special
+ # TODO once we fix up TypeNode, we won't need this
mr[atom.tag] = fact.object
- else
- mr[atom.tag] = fact.object.send(atom.method)
+ elsif !mr.key?(atom.tag) and atom.method
+ # TODO it should be possible to get rid of this, and just capture it in the Nodes above
+ mr[atom.tag] = fact.object.send(atom.method)
end
end
- super(MatchContext.new(fact,mr))
+ propagate_assert(MatchContext.new(fact,mr))
end
end
@@ -960,7 +988,14 @@ def clear_errors
end
class Assertion < Struct.new(:fact, :paths)
+ def add_tag(tag, value)
+ @tags ||= {}
+ @tags[tag] = value
+ end
+ def tags
+ @tags ? @tags : {}
+ end
end
end
View
@@ -257,6 +257,8 @@ def when(*args)
arg.deftemplate = deftemplate
@methods[arg.tag] = arg.name
atoms.push *arg.build_atoms(@tags, @methods, @when_counter)
+ elsif arg.kind_of? FunctionBuilder
+ atoms.push arg.build_atom(GeneratedTag.new, deftemplate)
elsif arg == false
raise 'The != operator is not allowed.'
else
@@ -317,8 +319,19 @@ def method_missing(method_id, *args, &block)
return ab
end
end
-
- class BindingBuilder
+
+ class FunctionBuilder
+ def initialize(args, block)
+ @args = args
+ @function = block
+ end
+
+ def build_atom(tag, template)
+ Core::FunctionAtom.new(tag, template, @args, @function)
+ end
+ end
+
+ class BindingBuilder
attr_accessor :tag, :method
def initialize(tag,method=nil)
@tag = tag
View
@@ -37,7 +37,23 @@ def b(variable_name)
end
def c(&block)
- return lambda(&block)
+ lambda(&block)
+ end
+
+ def f(args, block=nil)
+ if block.nil?
+ if !args.is_a?(Proc)
+ raise "You must provide a Proc!"
+ else
+ Ruleby::Ferrari::FunctionBuilder.new([], args)
+ end
+ else
+ if args.is_a?(Array)
+ Ruleby::Ferrari::FunctionBuilder.new(args, block)
+ else
+ Ruleby::Ferrari::FunctionBuilder.new([args], block)
+ end
+ end
end
def OR(*args)
View
@@ -0,0 +1,181 @@
+require 'spec_helper'
+
+class FuncFact
+ attr :value, true
+ attr :times, true
+ def initialize(v=nil); @value = v; @times = 0; end
+end
+
+include Ruleby
+
+class FunctionsRulebook < Rulebook
+ def rules_with_simple_function
+ rule [FuncFact, :a, f("b", c{|a, b| b == "b"})] do |v|
+ assert Success.new
+ end
+ end
+
+ def rules_with_function_testing_self(arg)
+ rule [FuncFact, :a, f(arg, c{|a, b| a.value == b})] do |v|
+ assert Success.new
+ end
+ end
+
+ def rules_that_share_a_function
+ func = c{|a, b| a.times += 1; b == "foobar"}
+
+ rule [FuncFact, :a, f("foobar", func)] do |v|
+ assert Success.new
+ end
+
+ rule [FuncFact, :a, f("foobar", func)] do |v|
+ assert Success.new
+ end
+ end
+
+ def rules_with_many_args_function
+ rule [FuncFact, :a, f([1, 2, 3, 4], c{|a, b, c, d, e| b < e})] do |v|
+ assert Success.new
+ end
+ end
+
+ def rules_with_no_args_function
+ rule [FuncFact, :a, f(c{|a| !a.nil?})] do |v|
+ assert Success.new
+ end
+ end
+end
+
+describe Ruleby::Rulebook do
+
+ describe "#f" do
+ context "rules_with_simple_function" do
+ subject do
+ engine :engine do |e|
+ FunctionsRulebook.new(e).rules_with_simple_function
+ end
+ end
+
+ context "with one FuncFact" do
+ before do
+ subject.assert FuncFact.new
+ subject.match
+ end
+
+ it "should match once" do
+ r = subject.retrieve Success
+ r.size.should == 1
+ subject.errors.should == []
+ end
+ end
+ end
+
+ context "rules_with_function_testing_self(:foo)" do
+ subject do
+ engine :engine do |e|
+ FunctionsRulebook.new(e).rules_with_function_testing_self(:foo)
+ end
+ end
+
+ context "with one FuncFact" do
+ before do
+ subject.assert FuncFact.new(:foo)
+ subject.match
+ end
+
+ it "should match once" do
+ r = subject.retrieve Success
+ r.size.should == 1
+ subject.errors.should == []
+ end
+ end
+ end
+
+ context "rules_with_function_testing_self(:bar)" do
+ subject do
+ engine :engine do |e|
+ FunctionsRulebook.new(e).rules_with_function_testing_self(:bar)
+ end
+ end
+
+ context "with one FuncFact" do
+ before do
+ subject.assert FuncFact.new(:foo)
+ subject.match
+ end
+
+ it "should not match " do
+ r = subject.retrieve Success
+ r.size.should == 0
+ subject.errors.should == []
+ end
+ end
+ end
+
+ context "rules_that_share_a_function" do
+ subject do
+ engine :engine do |e|
+ FunctionsRulebook.new(e).rules_that_share_a_function
+ end
+ end
+
+ context "with one FuncFact" do
+ before do
+ @f = FuncFact.new
+ subject.assert @f
+ subject.match
+ end
+
+ it "should match, node should be shared, function should be evaled once" do
+ r = subject.retrieve Success
+ r.size.should == 2
+ subject.errors.should == []
+
+ @f.times.should == 1
+ end
+ end
+ end
+
+ context "rules_with_many_args_function" do
+ subject do
+ engine :engine do |e|
+ FunctionsRulebook.new(e).rules_with_many_args_function
+ end
+ end
+
+ context "with one FuncFact" do
+ before do
+ subject.assert FuncFact.new
+ subject.match
+ end
+
+ it "should match once" do
+ r = subject.retrieve Success
+ r.size.should == 1
+ subject.errors.should == []
+ end
+ end
+ end
+
+ context "rules_with_no_args_function" do
+ subject do
+ engine :engine do |e|
+ FunctionsRulebook.new(e).rules_with_no_args_function
+ end
+ end
+
+ context "with one FuncFact" do
+ before do
+ subject.assert FuncFact.new
+ subject.match
+ end
+
+ it "should match once" do
+ r = subject.retrieve Success
+ r.size.should == 1
+ subject.errors.should == []
+ end
+ end
+ end
+ end
+end
View
@@ -28,7 +28,6 @@ def rules
end
it "should have matched" do
- subject.print
subject.errors.should == []
subject.retrieve(Success).size.should == 1
end
Oops, something went wrong.

0 comments on commit c017244

Please sign in to comment.