# # 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 query s query_clauses r:returning_clause? '?' { def ast Compiler::FactType.new nil, [], query_clauses.ast, (r.empty? ? nil : r) end } end rule fact_type s each:(each)? # Chew the "each" or it will get accepted as a quantifier name:(s term_definition_name mapping_pragmas is_where)? # Name of objectifying entity type anonymous_fact_type { def ast ft = anonymous_fact_type.ast if !name.empty? # "each" is often used, and doesn't imply uniqueness ft.name = name.term_definition_name.value pragmas = name.mapping_pragmas.value pragmas << 'independent' if name.is_where.independent ft.pragmas = pragmas elsif !each.empty? # Handle the implied mandatory constraint on the appropriate role first_reading = ft.clauses[0] refs = first_reading.refs raise "Ambiguous 'each' implies mandatory on fact type of arity #{refs.size}" unless refs.size == 2 q = refs[-1].quantifier if q q.min = 1 # Make the existing quantifier mandatory else refs[-1].quantifier = q = Compiler::Quantifier.new(1, nil) end end ft end } end rule anonymous_fact_type query_clauses ctail:( (':' / where) s a:query_clauses s)? returning_clause? s ';' { def ast clauses_ast = query_clauses.ast conditions = !ctail.empty? ? ctail.a.ast : [] returning = respond_to?(:returning_clause) ? returning_clause.ast : nil value_derivation = clauses_ast.detect{|r| r.is_equality_comparison} if !value_derivation and conditions.empty? and clauses_ast.detect{|r| r.includes_literals} raise "Fact instances may not contain conditions" unless conditions.empty? && !returning Compiler::Fact.new clauses_ast elsif (clauses_ast.size == 1 && clauses_ast[0].phrases.size == 1 && (popname = clauses_ast[0].phrases[0]) && !popname.is_a?(Compiler::Reference) && conditions.detect{|r| r.includes_literals} ) Compiler::Fact.new conditions, popname else Compiler::FactType.new nil, clauses_ast, conditions, returning end end } end rule query_clauses qualified_clauses # REVISIT: This creates no precedence between and/or, which could cause confusion. # Should disallow mixed conjunctions - using a sempred? ftail:( conjunction:(',' / and / or ) s qualified_clauses s )* { def ast clauses_ast = qualified_clauses.ast ftail.elements.each{|e| conjunction = e.conjunction.text_value # conjunction = 'and' if conjunction == ',' # ',' means AND, but disallows left-contractions clauses_ast += e.qualified_clauses.ast(conjunction) } clauses_ast end } end rule returning_clause returning s return (s ',' s return)* end rule return ordering_prefix? phrase+ end rule qualified_clauses s certainty s contracted_clauses s p:post_qualifiers? s c:context_note? { def ast(conjunction = nil) r = contracted_clauses.ast # An array of clause asts r[0].conjunction = conjunction # pre-qualifiers apply to the first clause, post_qualifiers and context_note to the last # REVISIT: This may be incorrect where the last is a nested clause r[0].certainty = certainty.value r[-1].qualifiers += p.list unless p.empty? r[-1].context_note = c.ast unless c.empty? r end } end rule certainty negative_prefix { def value; false; end } / maybe { def value; nil; end } / definitely { def value; true; end } / '' { def value; true; end } 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 / stronglyintransitive / transitive / acyclic / symmetric / asymmetric / antisymmetric / reflexive / irreflexive end rule clauses_list clauses tail:( ',' s clauses )* { def ast [clauses.ast, *tail.elements.map{|e| e.clauses.ast }] end } end rule clauses contracted_clauses s tail:( and s contracted_clauses s )* { def ast clauses = contracted_clauses.ast tail.elements.map{|e| clauses += e.contracted_clauses.ast } clauses end } end rule contracted_clauses comparison / ( contraction # A contraction will terminate this repetition by eating to the end / phrase )+ { def ast asts = elements.map{ |r| r.ast } contracted_clauses = [] qualifiers = [] if asts[-1].is_a?(Array) # A contraction (Array of [role, qualifiers, *clauses]) contracted_clauses = asts.pop # Pull off the contracted_clauses contracted_role = contracted_clauses.shift qualifiers = contracted_clauses.shift asts.push(contracted_role) # And replace it by the role removed from the contracted_clauses end clause_ast = Compiler::Clause.new(asts, qualifiers) [clause_ast] + contracted_clauses end } end rule contraction reading_contraction / condition_contraction end rule reading_contraction role p:post_qualifiers? conjunction:(that/who) s certainty s contracted_clauses s { def ast # contracted_clauses.ast will return an array of Clauses, but the first clause is special. We must: # * prepend a new role (we get the Role to build *two* ast nodes) # * attach the qualifiers clauses_ast = contracted_clauses.ast clauses_ast[0].conjunction = conjunction.text_value clauses_ast[0].phrases.unshift(role.ast) clauses_ast[0].certainty = certainty.value # A contraction returns an array containing: # * a role AST # * a qualifiers array # * an array of Clauses [role.ast, p.empty? ? [] : p.list] + clauses_ast end } end rule condition_contraction role pq:post_qualifiers? certainty s comparator s e2:expression !phrase # The contracted_clauses must not continue here! { def ast c = Compiler::Comparison.new(comparator.text_value, role.ast, e2.ast, certainty.value) c.conjunction = comparator.text_value [ role.ast, pq.empty? ? [] : pq.list, c ] end } end rule comparison e1:expression s certainty s comparator s contraction p:post_qualifiers? { def ast role, qualifiers, *clauses_ast = *contraction.ast clauses_ast[0].qualifiers += p.list unless p.empty? # apply post_qualifiers to the contracted clause # clauses_ast[0].conjunction = 'and' # AND is implicit for a contraction c = Compiler::Comparison.new(comparator.text_value, e1.ast, role, certainty.value) [c] + clauses_ast end } / certainty e1:expression s comparator s e2:expression # comparisons have no post-qualifiers: p:post_qualifiers? { def ast c = Compiler::Comparison.new(comparator.text_value, e1.ast, e2.ast, certainty.value) [c] end } end rule comparator '<=' / '<>' / '<' / '=' / '>=' / '>' / '!=' end rule phrase 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 def node_type; :linking; end } / # A normal non-term !non_phrase id s { def ast id.value end def node_type; :linking; end } end rule role aggregate / simple_role end rule aggregate aggregate:id s agg_of s term_or_unary s agg_in s # REVISIT: this term may need to pre-scanned in the qualified_clauses '(' qualified_clauses s ')' # REVISIT: Need to test to verify this is the right level (not query_clauses, etc) { def ast raise "Not implemented: AST for '#{aggregate.text_value} of #{term_or_unary.text_value}'" # This returns just the role with the nested clauses, which doesn't even work: term.ast( nil, # No quantifier nil, # No function call nil, # No role_name nil, # No value_constraint nil, # No literal qualified_clauses.ast ) end } end rule role_quantifier quantifier mapping_pragmas enforcement cn:context_note? { def ast Compiler::Quantifier.new( quantifier.value[0], quantifier.value[1], enforcement.ast, cn.empty? ? nil : cn.ast, mapping_pragmas.value ) end } end # This is the rule that causes most back-tracking. I think you can see why. rule simple_role q:role_quantifier? player:derived_variable lr:( literal u:unit? / value_constraint enforcement )? oj:objectification_step? { def ast if !q.empty? && q.quantifier.value quantifier = q.ast end if !lr.empty? if lr.respond_to?(:literal) literal = Compiler::Literal.new(lr.literal.value, lr.u.empty? ? nil : lr.u.text_value) end value_constraint = Compiler::ValueConstraint.new(lr.value_constraint.ast, lr.enforcement.ast) if lr.respond_to?(:value_constraint) raise "It is not permitted to provide both a literal value and a value constraint" if value_constraint and literal end nested_clauses = if oj.empty? nil else ast = oj.ast ast[0].conjunction = 'where' ast end player.ast(quantifier, value_constraint, literal, nested_clauses) end } end rule objectification_step '(' s in_which s facts:query_clauses s ')' s { def ast facts.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 end end end