# # ActiveFacts CQL Parser. # Parse rules relating to FactType definitions. # # Copyright (c) 2009 Clifford Heath. Read the LICENSE file. # module ActiveFacts module CQL grammar FactTypes rule named_fact_type s term_definition_name s is s mapping_pragmas where # REVISIT: Need a place to put mapping pragmas like [independent] anonymous_fact_type { def ast ft = anonymous_fact_type.ast ft.name = term_definition_name.value ft end } end rule anonymous_fact_type fact_clause ftail:( (',' / and ) s fact_clause s )* ctail:( (':' / where) s c:conditions s)? returning_clause? s ';' { def ast readings = fact_clause.ast ftail.elements.each{|e| readings += e.fact_clause.ast } conditions = !ctail.empty? ? ctail.c.ast : [] returning = respond_to?(:returning_clause) ? returning_clause.ast : nil if (conditions.empty? && readings.detect{|r| r.includes_literals}) raise "Fact instances may not contain conditions" unless conditions.empty? && !returning Compiler::Fact.new readings elsif (readings.size == 1 && readings[0].phrases.size == 1 && (popname = readings[0].phrases[0]) && !popname.is_a?(Compiler::RoleRef) && conditions.detect{|r| r.includes_literals} ) Compiler::Fact.new conditions, popname else Compiler::FactType.new nil, readings, conditions, returning end end } end rule returning_clause returning return (',' return)* end rule return by order 'REVISIT: return' end rule conditions head:condition s tail:( (',' s / and S) next:condition s )* { def ast conditions = head.ast tail.elements.each{|i| conditions += i.next.ast} conditions end } end rule condition head:clause s # tail:(or S alternate:clause s )* { def ast head.ast # Compiler::Alternates.new(head.ast, *tail.elements.map{|i| i.alternate.ast}) # REVISIT: alternate conditions are not yet implemented end } end rule clause # REVISIT: No context for comparisons, yet comparison / fact_clause end rule fact_clause s q:qualifier? s reading s p:post_qualifiers? s c:context_note? { def ast r = reading.ast # An array of readings r[0].qualifiers << q.text_value unless q.empty? r[-1].qualifiers += p.list unless p.empty? r[-1].context_note = c.ast unless c.empty? r end } end rule qualifier maybe / definitely end rule post_qualifiers '[' s q0:post_qualifier tail:( s ',' s q1:post_qualifier )* s ']' s { def list [q0.text_value, *tail.elements.map{|e| e.q1.text_value}] end } end rule post_qualifier static / transient / intransitive / transitive / acyclic / symmetric / asymmetric / antisymmetric / reflexive / irreflexive end rule reading ( contracted_reading / role # A role reference containing a term, perhaps with attached paraphernalia / # A hyphenated non-term. Important: no embedded spaces id tail:('-' !term id)+ s { def ast [id.value, *tail.elements.map{|e| e.id.value}]*"-" end } / # A normal non-term !non_role_word id s { def ast id.value end } )+ { def ast asts = elements.map{ |r| r.ast } contraction = [] qualifiers = [] if asts[-1].is_a?(Array) # A contracted_reading (Array of [role, qualifiers, *readings]) contraction = asts.pop # Pull off the contraction contracted_role = contraction.shift qualifiers = contraction.shift asts.push(contracted_role) # And replace it by the role removed from the contraction end reading = Compiler::Reading.new(asts) reading.qualifiers += qualifiers [reading] + contraction end } end # REVISIT: and later, similarly for contracted conditions (including comparisons) rule contracted_reading role p:post_qualifiers? that s q:qualifier? s reading s { def ast # reading.ast will return an array of Readings, but the first reading is special. We must: # * prepend a new role # * attach the qualifiers readings = reading.ast readings[0].phrases.unshift(role.ast) readings[0].qualifiers << q.text_value unless q.empty? # Add maybe/definitely # A contracted_reading returns an array containing: # * a role AST # * a qualifiers array # * an array of Readings [role.ast, p.empty? ? [] : p.list] + readings end } end # This is the rule that causes most back-tracking. I think you can see why. # When we have an expression, we will come down here perhaps multiple times, # but find no way out as soon as we hit the trailing non_role. rule role q:(quantifier enforcement)? player:term func:function_call? s role_id:(role_name / subscript )? lr:( literal u:unit? / value_constraint enforcement )? oj:objectification_join? !non_role # If we integrate fact_clauses with comparisons, this can go. { def ast if !q.empty? && q.quantifier.value quantifier = Compiler::Quantifier.new(q.quantifier.value[0], q.quantifier.value[1], q.enforcement.ast) end role_name = role_id.empty? ? nil : role_id.value function_call = nil if !lr.empty? if lr.respond_to?(:literal) literal = lr.literal.value raise "Literals with units are not yet processed" unless lr.u.empty? end value_constraint = Compiler::ValueConstraint.new(lr.value_constraint.ranges, lr.enforcement.ast) if lr.respond_to?(:value_constraint) raise "It is not permitted to provide both a literal value and a value restriction" if value_constraint and literal end objectification_join = oj.empty? ? nil : oj.ast player.ast(quantifier, function_call, role_name, value_constraint, literal, objectification_join) end } end rule objectification_join '(' s where s fact_clause s ')' s { def ast fact_clause.ast end } end rule role_name '(' s as S r:term s ')' s { def value; r.value; end } end rule subscript '(' s i:([1-9] [0-9]*) s ')' s { def value; i.text_value.to_i; end } end rule non_role # Any of these is illegal in or following a reading (they indicate a comparison is coming). Later, this will change: comparator / add_op / mul_op end end end end