diff --git a/etc/n3.ebnf b/etc/n3.ebnf index 7e48f2e..b323c68 100644 --- a/etc/n3.ebnf +++ b/etc/n3.ebnf @@ -44,6 +44,7 @@ [18] pathItem ::= iri | blankNode + | quantifiedVar | quickVar | collection | blankNodePropertyList @@ -69,7 +70,7 @@ [26] iri ::= IRIREF | prefixedName - [27] iriList ::= iri ( ',' iri )* + [27] varList ::= (iri | quantifiedVar) ( ',' (iri | quantifiedVar) )* [28] prefixedName ::= PNAME_LN | PNAME_NS # PNAME_NS will be matched for ':' (i.e., "empty") prefixedNames @@ -78,19 +79,23 @@ [29] blankNode ::= BLANK_NODE_LABEL | ANON - [30] quickVar ::= QUICK_VAR_NAME + [30] quantifiedVar ::= QUANTIFIED_VAR_NAME + + [31] quickVar ::= QUICK_VAR_NAME # only made this a parser rule for consistency # (all other path-items are also parser rules) - [31] existential ::= '@forSome' iriList - - [32] universal ::= '@forAll' iriList + [32] existential ::= '@forSome' varList + # iriList is deprecated + + [33] universal ::= '@forAll' varList + # iriList is deprecated @terminals - [33] BOOLEAN_LITERAL ::= 'true' | 'false' + [34] BOOLEAN_LITERAL ::= 'true' | 'false' - [34] STRING ::= STRING_LITERAL_LONG_SINGLE_QUOTE + [35] STRING ::= STRING_LITERAL_LONG_SINGLE_QUOTE | STRING_LITERAL_LONG_QUOTE | STRING_LITERAL_QUOTE | STRING_LITERAL_SINGLE_QUOTE @@ -110,12 +115,13 @@ [157s] STRING_LITERAL_SINGLE_QUOTE ::= "'" ( [^#x27#x5C#xA#xD] | ECHAR | UCHAR )* "'" [158s] STRING_LITERAL_LONG_SINGLE_QUOTE ::= "'''" ( ( "'" | "''" )? ( [^'\] | ECHAR | UCHAR ) )* "'''" [159s] STRING_LITERAL_LONG_QUOTE ::= '"""' ( ( '"' | '""' )? ( [^"\] | ECHAR | UCHAR ) )* '"""' - [35] UCHAR ::= ( "\u" HEX HEX HEX HEX ) | ( "\U" HEX HEX HEX HEX HEX HEX HEX HEX ) + [36] UCHAR ::= ( "\u" HEX HEX HEX HEX ) | ( "\U" HEX HEX HEX HEX HEX HEX HEX HEX ) [160s] ECHAR ::= "\" [tbnrf\"'] [162s] WS ::= #x20 | #x9 | #xD | #xA [163s] ANON ::= '[' WS* ']' - [36] QUICK_VAR_NAME ::= "?" PN_LOCAL + [37] QUICK_VAR_NAME ::= "?" PN_LOCAL /* Allows fuller character set */ + [38] QUANTIFIED_VAR_NAME ::= "$" PN_LOCAL [164s] PN_CHARS_BASE ::= [A-Z] | [a-z] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x02FF] | [#x0370-#x037D] | [#x037F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] @@ -124,8 +130,8 @@ [165s] PN_CHARS_U ::= PN_CHARS_BASE | '_' [167s] PN_CHARS ::= PN_CHARS_U | "-" | [0-9] | #x00B7 | [#x0300-#x036F] | [#x203F-#x2040] /* BASE and PREFIX must be case-insensitive, hence these monstrosities */ - [37] BASE ::= ('B'|'b') ('A'|'a') ('S'|'s') ('E'|'e') - [38] PREFIX ::= ('P'|'p') ('R'|'r') ('E'|'e') ('F'|'f') ('I'|'i') ('X'|'x') + [39] BASE ::= ('B'|'b') ('A'|'a') ('S'|'s') ('E'|'e') + [40] PREFIX ::= ('P'|'p') ('R'|'r') ('E'|'e') ('F'|'f') ('I'|'i') ('X'|'x') [168s] PN_PREFIX ::= PN_CHARS_BASE ( ( PN_CHARS | "." )* PN_CHARS )? [169s] PN_LOCAL ::= ( PN_CHARS_U | ':' | [0-9] | PLX ) ( ( PN_CHARS | '.' | ':' | PLX )* ( PN_CHARS | ':' | PLX ) ) ? [170s] PLX ::= PERCENT | PN_LOCAL_ESC @@ -133,7 +139,7 @@ [172s] HEX ::= [0-9] | [A-F] | [a-f] [173s] PN_LOCAL_ESC ::= '\' ( '_' | '~' | '.' | '-' | '!' | '$' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '/' | '?' | '#' | '@' | '%' ) - [39] COMMENT ::= ('#' - '#x') [^#xA#xC#xD]* + [41] COMMENT ::= ('#' - '#x') [^#xA#xC#xD]* # Ignore all whitespace and comments between non-terminals @pass ( WS | COMMENT )* diff --git a/etc/n3.sxp b/etc/n3.sxp index 102eed7..de279b6 100644 --- a/etc/n3.sxp +++ b/etc/n3.sxp @@ -34,15 +34,16 @@ (rule numericLiteral "24" (alt DOUBLE DECIMAL INTEGER)) (rule rdfLiteral "25" (seq STRING (opt (alt LANGTAG (seq "^^" iri))))) (rule iri "26" (alt IRIREF prefixedName)) - (rule iriList "27" (seq iri (star (seq "," iri)))) + (rule varList "27" (seq (alt iri quantifiedVar) (star (seq "," (alt iri quantifiedVar))))) (rule prefixedName "28" (alt PNAME_LN PNAME_NS)) (rule blankNode "29" (alt BLANK_NODE_LABEL ANON)) - (rule quickVar "30" (seq QUICK_VAR_NAME)) - (rule existential "31" (seq "@forSome" iriList)) - (rule universal "32" (seq "@forAll" iriList)) + (rule quantifiedVar "30" (seq QUANTIFIED_VAR_NAME)) + (rule quickVar "31" (seq QUICK_VAR_NAME)) + (rule existential "32" (seq "@forSome" varList)) + (rule universal "33" (seq "@forAll" varList)) (terminals _terminals (seq)) - (terminal BOOLEAN_LITERAL "33" (alt "true" "false")) - (terminal STRING "34" + (terminal BOOLEAN_LITERAL "34" (alt "true" "false")) + (terminal STRING "35" (alt STRING_LITERAL_LONG_SINGLE_QUOTE STRING_LITERAL_LONG_QUOTE STRING_LITERAL_QUOTE STRING_LITERAL_SINGLE_QUOTE )) (terminal IRIREF "139s" @@ -70,12 +71,13 @@ (seq "'''" (star (seq (opt (alt "'" "''")) (alt (range "^'\\") ECHAR UCHAR))) "'''")) (terminal STRING_LITERAL_LONG_QUOTE "159s" (seq "\"\"\"" (star (seq (opt (alt "\"" "\"\"")) (alt (range "^\"\\") ECHAR UCHAR))) "\"\"\"")) - (terminal UCHAR "35" + (terminal UCHAR "36" (alt (seq "\\u" HEX HEX HEX HEX) (seq "\\U" HEX HEX HEX HEX HEX HEX HEX HEX))) (terminal ECHAR "160s" (seq "\\" (range "tbnrf\\\"'"))) (terminal WS "162s" (alt (hex "#x20") (hex "#x9") (hex "#xD") (hex "#xA"))) (terminal ANON "163s" (seq "[" (star WS) "]")) - (terminal QUICK_VAR_NAME "36" (seq "?" PN_LOCAL)) + (terminal QUICK_VAR_NAME "37" (seq "?" PN_LOCAL)) + (terminal QUANTIFIED_VAR_NAME "38" (seq "$" PN_LOCAL)) (terminal PN_CHARS_BASE "164s" (alt (range "A-Z") @@ -99,8 +101,8 @@ (hex "#x00B7") (range "#x0300-#x036F") (range "#x203F-#x2040")) ) - (terminal BASE "37" (seq (alt "B" "b") (alt "A" "a") (alt "S" "s") (alt "E" "e"))) - (terminal PREFIX "38" + (terminal BASE "39" (seq (alt "B" "b") (alt "A" "a") (alt "S" "s") (alt "E" "e"))) + (terminal PREFIX "40" (seq (alt "P" "p") (alt "R" "r") (alt "E" "e") (alt "F" "f") (alt "I" "i") (alt "X" "x"))) (terminal PN_PREFIX "168s" (seq PN_CHARS_BASE (opt (seq (star (alt PN_CHARS ".")) PN_CHARS)))) @@ -115,5 +117,5 @@ (seq "\\" (alt "_" "~" "." "-" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" "/" "?" "#" "@" "%" )) ) - (terminal COMMENT "39" (seq (diff "#" "#x") (star (range "^#xA#xC#xD")))) + (terminal COMMENT "41" (seq (diff "#" "#x") (star (range "^#xA#xC#xD")))) (pass _pass (star (alt WS COMMENT)))) diff --git a/lib/rdf/n3/reader.rb b/lib/rdf/n3/reader.rb index 1a49f6c..c5a157e 100644 --- a/lib/rdf/n3/reader.rb +++ b/lib/rdf/n3/reader.rb @@ -192,6 +192,7 @@ def each_triple terminal(:BASE, BASE) terminal(:LANGTAG, LANGTAG) terminal(:QUICK_VAR_NAME, QUICK_VAR_NAME, unescape: true) + terminal(:QUANTIFIED_VAR_NAME, QUANTIFIED_VAR_NAME, unescape: true) private ## @@ -419,6 +420,7 @@ def read_path pathtail[:pathitem] = prod(:pathItem) do read_iri || read_blankNode || + read_quantifiedVar || read_quickVar || read_collection || read_blankNodePropertyList || @@ -638,17 +640,35 @@ def read_blankNode end end + ## + # Read a quantifiedVar. + # + # [30] quantifiedVar ::= QUANTIFIED_VAR_NAME + # + # @param [Boolean] existential. Set if called for either `@forSome` or `@forAll`. Otherwise, it will have been cached. + # @return [RDF::Query::Variable] + def read_quantifiedVar(existential: false) + if @lexer.first.type == :QUANTIFIED_VAR_NAME + prod(:quantifiedVar) do + token = @lexer.shift + value = token.value[1..-1] + iri = ns(nil, "#{value}_quant") + variables[formulae.last][iri] ||= univar(iri, scope: formulae.last, existential: existential) + end + end + end + ## # Read a quickVar, having global scope. # - # [30] quickVar ::= QUICK_VAR_NAME + # [31] quickVar ::= QUICK_VAR_NAME # # @return [RDF::Query::Variable] def read_quickVar if @lexer.first.type == :QUICK_VAR_NAME prod(:quickVar) do token = @lexer.shift - value = token.value.sub('?', '') + value = token.value[1..-1] iri = ns(nil, "#{value}_quick") variables[nil][iri] ||= univar(iri, scope: nil) end @@ -658,19 +678,20 @@ def read_quickVar ## # Read a list of IRIs # - # [27] iriList ::= iri ( ',' iri )* + # [27] varList ::= (iri | quantifiedVar) ( ',' (iri | quantifiedVar) )* # + # @param [Boolean] existential. Set if called for either `@forSome` or `@forAll`. Otherwise, it will have been cached. # @return [Array] the list of IRIs - def read_irilist - iris = [] - prod(:iriList, %{,}) do - while iri = read_iri - iris << iri + def read_varList(existential: false) + vars = [] + prod(:varlist, %{,}) do + while var = read_quantifiedVar(existential: existential) || read_iri + vars << var break unless @lexer.first === ',' @lexer.shift while @lexer.first === ',' end end - iris + vars end ## @@ -690,12 +711,16 @@ def read_irilist def read_uniext if %w(@forSome @forAll).include?(@lexer.first.value) token = @lexer.shift - prod(token === '@forAll' ? :universal : :existential) do - iri_list = read_irilist - iri_list.each do |iri| + uniext = token === '@forAll' ? :universal : :existential + prod(uniext) do + var_list = read_varList(existential: uniext == :existential) + var_list.each do |label| + log_warn( + "[DEPRECATION] The use of IRIs as quantified variables is deprecated; use the '$var' form instead." + ) if label.iri? # Note, this might re-create an equivalent variable already defined in this formula, and replaces an equivalent variable that may have been defined in the parent formula. - var = univar(iri, scope: formulae.last, existential: token === '@forSome') - add_var_to_formula(formulae.last, iri, var) + var = univar(label, scope: formulae.last, existential: uniext == :existential) + add_var_to_formula(formulae.last, label, var) end end end @@ -772,8 +797,8 @@ def bnode(label = nil) # If not in ground formula, note scope, and if existential def univar(label, scope:, existential: false) - value = existential ? "#{label}_ext" : label - value = "#{value}#{scope.id}" if scope + return label if label.is_a?(RDF::Query::Variable) + value = scope ? "#{label}#{scope.id}" : label RDF::Query::Variable.new(value, existential: existential) end diff --git a/lib/rdf/n3/terminals.rb b/lib/rdf/n3/terminals.rb index 4dae60a..eac7923 100644 --- a/lib/rdf/n3/terminals.rb +++ b/lib/rdf/n3/terminals.rb @@ -69,6 +69,7 @@ module Terminals # 29t BASE = /@?base/ui.freeze QUICK_VAR_NAME = /\?#{PN_LOCAL}/.freeze + QUANTIFIED_VAR_NAME = /\$#{PN_LOCAL}/.freeze # 161s WS = /(?:\s|(?:#[^\n\r]*))+/um.freeze diff --git a/lib/rdf/n3/writer.rb b/lib/rdf/n3/writer.rb index 58a4360..c3d326c 100644 --- a/lib/rdf/n3/writer.rb +++ b/lib/rdf/n3/writer.rb @@ -335,13 +335,25 @@ def start_document # Universals and extentials at top-level unless @universals.empty? log_debug("start_document: universals") { @universals.inspect} - terms = @universals.map {|v| format_uri(RDF::URI(v.name.to_s))} + terms = @universals.map do |v| + if v.to_s.include?('_quant') + '$' + RDF::URI(v.name).fragment.sub(/_quant$/, '').sub(/_ext$/, '') + else + format_uri(RDF::URI(v.name.to_s.sub(/_ext$/, ''))) + end + end @output.write("@forAll #{terms.join(', ')} .\n") end unless @existentials.empty? log_debug("start_document: existentials") { @existentials.inspect} - terms = @existentials.map {|v| format_uri(RDF::URI(v.name.to_s.sub(/_ext$/, '')))} + terms = @existentials.map do |v| + if v.to_s.include?('_quant') + '$' + RDF::URI(v.name).fragment.sub(/_quant$/, '').sub(/_ext$/, '') + else + format_uri(RDF::URI(v.name.to_s.sub(/_ext$/, ''))) + end + end @output.write("@forSome #{terms.join(', ')} .\n") end end @@ -523,6 +535,8 @@ def p_term(resource, position) l = if resource.is_a?(RDF::Query::Variable) if resource.to_s.end_with?('_quick') '?' + RDF::URI(resource.name).fragment.sub(/_quick$/, '') + elsif resource.to_s.include?('_quant') + '$' + RDF::URI(resource.name).fragment.sub(/_quant$/, '') else format_term(RDF::URI(resource.name.to_s.sub(/_ext$/, ''))) end diff --git a/spec/reader_spec.rb b/spec/reader_spec.rb index cdecab6..996d005 100644 --- a/spec/reader_spec.rb +++ b/spec/reader_spec.rb @@ -490,17 +490,27 @@ end context "patterns" do - it "substitutes variable for URI with @forAll" do + it "warns of deprecation on @forAll with URIs" do n3 = %(@forAll :x . :x :y :z .) g = parse(n3, base_uri: "http://a/b") statement = g.statements.first expect(statement.subject).to be_variable expect(statement.predicate.to_s).to eq "http://a/b#y" expect(statement.object.to_s).to eq "http://a/b#z" + expect(logger.to_s).to include("[DEPRECATION]") end - it "substitutes variable for URIs with @forAll" do - n3 = %(@forAll :x, :y, :z . :x :y :z .) + it "substitutes variable for variable with @forAll" do + n3 = %(@forAll $x . $x :y :z .) + g = parse(n3, base_uri: "http://a/b") + statement = g.statements.first + expect(statement.subject).to be_variable + expect(statement.predicate.to_s).to eq "http://a/b#y" + expect(statement.object.to_s).to eq "http://a/b#z" + end + + it "substitutes variable for variables with @forAll" do + n3 = %(@forAll $x, $y, $z . $x $y $z .) g = parse(n3, base_uri: "http://a/b") statement = g.statements.first expect(statement.subject).to be_variable @@ -522,17 +532,27 @@ expect(statement.subject).not_to equal statement.object end - it "substitutes node for URI with @forSome" do + it "warns of deprecation on @forSome with URIs" do n3 = %(@forSome :x . :x :y :z .) g = parse(n3, base_uri: "http://a/b") statement = g.statements.first + expect(statement.subject).to be_variable + expect(statement.predicate.to_s).to eq "http://a/b#y" + expect(statement.object.to_s).to eq "http://a/b#z" + expect(logger.to_s).to include("[DEPRECATION]") + end + + it "substitutes node for variable with @forSome" do + n3 = %(@forSome $x . $x :y :z .) + g = parse(n3, base_uri: "http://a/b") + statement = g.statements.first expect(statement.subject).to be_variable, logger.to_s expect(statement.predicate.to_s).to eq "http://a/b#y" expect(statement.object.to_s).to eq "http://a/b#z" end - it "substitutes node for URIs with @forSome" do - n3 = %(@forSome :x, :y, :z . :x :y :z .) + it "substitutes node for variables with @forSome" do + n3 = %(@forSome $x, $y, $z . $x $y $z .) g = parse(n3, base_uri: "http://a/b") statement = g.statements.first expect(statement.subject).to be_variable diff --git a/spec/reasoner_spec.rb b/spec/reasoner_spec.rb index 8724f1e..996497a 100644 --- a/spec/reasoner_spec.rb +++ b/spec/reasoner_spec.rb @@ -108,9 +108,9 @@ { "r1" => { input: %( - @forAll :a, :b. + @forAll $a, $b. ( "one" "two" ) a :whatever. - { (:a :b) a :whatever } log:implies { :a a :SUCCESS. :b a :SUCCESS }. + { ($a $b) a :whatever } log:implies { $a a :SUCCESS. $b a :SUCCESS }. ), expect: %( ( "one" "two" ) a :whatever. @@ -213,7 +213,7 @@ }, #"quantifiers-limited-a2" => { # input: %( - # {{ :foo :bar :baz } log:includes { @forSome :foo. :foo :bar :baz }} + # {{ :foo :bar :baz } log:includes { @forSome $foo. $foo :bar :baz }} # => { :testa2 a :success } . # ), # expect: %( @@ -223,7 +223,7 @@ #}, #"quantifiers-limited-b2" => { # input: %( - # {{ @forSome :foo. :foo :bar :baz } log:includes {@forSome :foo. :foo :bar :baz }} + # {{ @forSome $foo. $foo :bar :baz } log:includes {@forSome $foo. $foo :bar :baz }} # => { :testb2 a :success } . # ), # expect: %( @@ -264,7 +264,7 @@ @prefix log: . @prefix : <#>. - @forAll :F. + @forAll $F. {""" @prefix : . @prefix crypto: . @@ -275,7 +275,7 @@ :foo :credential ; :forDocument ; :junk "32746213462187364732164732164321" . - """ log:parsedAsN3 :F} log:implies { :F a :result }. + """ log:parsedAsN3 $F} log:implies { $F a :result }. ), expect: %( @prefix rdf: . diff --git a/spec/writer_spec.rb b/spec/writer_spec.rb index 7113897..20a7e1c 100644 --- a/spec/writer_spec.rb +++ b/spec/writer_spec.rb @@ -615,20 +615,34 @@ describe "variables" do { - "@forAll": { + "@forAll (URI)": { input: %(@forAll :o. :s :p :o .), regexp: [ %r(@forAll :o \.), %r(:s :p :o \.), ] }, - "@forSome": { + "@forAll (var)": { + input: %(@forAll $o. :s :p $o .), + regexp: [ + %r(@forAll \$o \.), + %r(:s :p \$o \.), + ] + }, + "@forSome (URI)": { input: %(@forSome :o. :s :p :o .), regexp: [ %r(@forSome :o \.), %r(:s :p :o \.), ] }, + "@forSome (var)": { + input: %(@forSome $o. :s :p $o .), + regexp: [ + %r(@forSome \$o \.), + %r(:s :p \$o \.), + ] + }, "?o": { input: %(:s :p ?o .), regexp: [