Kirill Simonov avatar Kirill Simonov committed ee4fe6d

Enchanced syntax for infix function call.

The following constructs:
a:f
a:f b
a:f(b,c,...)
are equivalent respectively to:
f(a)
f(a,b)
f(a,b,c,...)

Comments (0)

Files changed (3)

src/htsql/tr/parse.py

         filter          ::= '?' test
         format          ::= '/' ':' identifier
 
-        element         ::= test ( '+' | '-' )*
-        test            ::= and_test ( '|' and_test )*
+        test            ::= test direction | test application | or_test
+        direction       ::= ( '+' | '-' )
+        application     ::= ':' identifier ( or_test | call )?
+        or_test         ::= and_test ( '|' and_test )*
         and_test        ::= implies_test ( '&' implies_test )*
         implies_test    ::= unary_test ( '->' unary_test )?
         unary_test      ::= '!' unary_test | comparison
         specifier       ::= atom ( '.' identifier call? )* ( '.' '*' )?
         atom            ::= '*' | selector | group | identifier call? | literal
 
-        group           ::= '(' element ')'
-        call            ::= '(' elements? ')'
-        selector        ::= '{' elements? '}'
-        elements        ::= element ( ',' element )* ','?
+        group           ::= '(' test ')'
+        call            ::= '(' tests? ')'
+        selector        ::= '{' tests? '}'
+        tests           ::= test ( ',' test )* ','?
 
         identifier      ::= NAME
         literal         ::= STRING | NUMBER
         return segment
 
 
-class ElementParser(Parser):
-    """
-    Parses an `element` production.
-    """
-
-    @classmethod
-    def process(cls, tokens):
-        # Parses the production:
-        #   element         ::= test ( '+' | '-' )*
-        element = TestParser << tokens
-        while tokens.peek(SymbolToken, ['+', '-']):
-            symbol_token = tokens.pop(SymbolToken, ['+', '-'])
-            symbol = symbol_token.value
-            mark = Mark.union(element, symbol_token)
-            element = OperatorSyntax(symbol, element, None, mark)
-        return element
-
-
 class TestParser(Parser):
     """
     Parses a `test` production.
 
     @classmethod
     def process(cls, tokens):
+        # Parses the productions:
+        #   test            ::= test direction | test application | or_test
+        #   direction       ::= ( '+' | '-' )
+        #   application     ::= ':' identifier ( or_test | call )?
+        test = OrTestParser << tokens
+        while tokens.peek(SymbolToken, ['+', '-', ':']):
+            if tokens.peek(SymbolToken, ['+', '-']):
+                symbol_token = tokens.pop(SymbolToken, ['+', '-'])
+                symbol = symbol_token.value
+                mark = Mark.union(test, symbol_token)
+                test = OperatorSyntax(symbol, test, None, mark)
+            else:
+                symbol_token = tokens.pop(SymbolToken, [':'])
+                identifier = IdentifierParser << tokens
+                arguments = [test]
+                if tokens.peek(SymbolToken, ['(']):
+                    tokens.pop(SymbolToken, ['('])
+                    while not tokens.peek(SymbolToken, [')']):
+                        argument = TestParser << tokens
+                        arguments.append(argument)
+                        if not tokens.peek(SymbolToken, [')']):
+                            tokens.pop(SymbolToken, [',', ')'])
+                    tail_token = tokens.pop(SymbolToken, [')'])
+                    mark = Mark.union(test, tail_token)
+                else:
+                    ahead = 0
+                    while tokens.peek(SymbolToken, ['+', '-'], ahead=ahead):
+                        ahead += 1
+                    if not (tokens.peek(SymbolToken,
+                                        [':', ',', ')', '}'], ahead=ahead) or
+                            tokens.peek(EndToken, ahead=ahead)):
+                        argument = OrTestParser << tokens
+                        arguments.append(argument)
+                    mark = Mark.union(test, identifier, arguments[-1])
+                test = FunctionOperatorSyntax(identifier, arguments, mark)
+        return test
+
+
+class OrTestParser(Parser):
+    """
+    Parses an `or_test` production.
+    """
+
+    @classmethod
+    def process(cls, tokens):
         # Parses the production:
-        #   test            ::= and_test ( '|' and_test )*
+        #   or_test         ::= and_test ( '|' and_test )*
         test = AndTestParser << tokens
         while tokens.peek(SymbolToken, ['|']):
             symbol_token = tokens.pop(SymbolToken, ['|'])
         #   expression      ::= term ( ( '+' | '-' ) term )*
         expression = TermParser << tokens
         # Here we perform a look-ahead to distinguish between productions:
-        #   element         ::= test ( '+' | '-' )*
+        #   test            ::= test direction | test application | or_test
+        #   direction       ::= ( '+' | '-' )
+        #   application     ::= ':' identifier ( or_test | call )?
         # and
         #   expression      ::= term ( ( '+' | '-' ) term )*
-        # We know that the FOLLOWS set of `element` consists of the symbols:
+        # We know that the FOLLOWS set of `test` consists of the symbols:
         #   ',', ')', and '}',
         # which never start the `term` non-terminal.
         while tokens.peek(SymbolToken, ['+', '-']):
             ahead = 1
             while tokens.peek(SymbolToken, ['+', '-'], ahead=ahead):
                 ahead += 1
-            if tokens.peek(SymbolToken, [',', ')', '}'], ahead=ahead):
+            if tokens.peek(SymbolToken, [':', ',', ')', '}'], ahead=ahead):
                 break
             symbol_token = tokens.pop(SymbolToken, ['+', '-'])
             symbol = symbol_token.value
                 right = FactorParser << tokens
                 mark = Mark.union(left, right)
                 expression = FunctionOperatorSyntax(identifier,
-                                                    left, right, mark)
+                                                    [left, right], mark)
         return expression
 
 
     def process(cls, tokens):
         # Parses the productions:
         #   specifier       ::= atom ( '.' identifier call? )* ( '.' '*' )?
-        #   call            ::= '(' elements? ')'
-        #   elements        ::= element ( ',' element )* ','?
+        #   call            ::= '(' test? ')'
+        #   tests           ::= test ( ',' test )* ','?
         expression = AtomParser << tokens
         while tokens.peek(SymbolToken, ['.'], do_pop=True):
             if tokens.peek(SymbolToken, ['*']):
                     tokens.pop(SymbolToken, ['('])
                     arguments = []
                     while not tokens.peek(SymbolToken, [')']):
-                        argument = ElementParser << tokens
+                        argument = TestParser << tokens
                         arguments.append(argument)
                         if not tokens.peek(SymbolToken, [')']):
                             tokens.pop(SymbolToken, [',', ')'])
     def process(cls, tokens):
         # Parses the productions:
         #   atom        ::= '*' | selector | group | identifier call? | literal
-        #   call        ::= '(' elements? ')'
-        #   elements    ::= element ( ',' element )* ','?
+        #   call        ::= '(' tests? ')'
+        #   tests       ::= tests ( ',' tests )* ','?
         #   literal     ::= STRING | NUMBER
         if tokens.peek(SymbolToken, ['*']):
             symbol_token = tokens.pop(SymbolToken, ['*'])
                 tokens.pop(SymbolToken, ['('])
                 arguments = []
                 while not tokens.peek(SymbolToken, [')']):
-                    argument = ElementParser << tokens
+                    argument = TestParser << tokens
                     arguments.append(argument)
                     if not tokens.peek(SymbolToken, [')']):
                         tokens.pop(SymbolToken, [',', ')'])
     @classmethod
     def process(self, tokens):
         # Parses the production:
-        #   group           ::= '(' element ')'
+        #   group           ::= '(' test ')'
         head_token = tokens.pop(SymbolToken, ['('])
-        expression = ElementParser << tokens
+        expression = TestParser << tokens
         tail_token = tokens.pop(SymbolToken, [')'])
         mark = Mark.union(head_token, tail_token)
         group = GroupSyntax(expression, mark)
     @classmethod
     def process(cls, tokens):
         # Parses the productions:
-        #   selector        ::= '{' elements? '}'
-        #   elements        ::= element ( ',' element )* ','?
+        #   selector        ::= '{' tests? '}'
+        #   tests           ::= test ( ',' test )* ','?
         head_token = tokens.pop(SymbolToken, ['{'])
-        elements = []
+        tests = []
         while not tokens.peek(SymbolToken, ['}']):
-            element = ElementParser << tokens
-            elements.append(element)
+            test = TestParser << tokens
+            tests.append(test)
             if not tokens.peek(SymbolToken, ['}']):
                 # We know it's not going to be '}', but we put it into the list
                 # of accepted values to generate a better error message.
                 tokens.pop(SymbolToken, [',', '}'])
         tail_token = tokens.pop(SymbolToken, ['}'])
         mark = Mark.union(head_token, tail_token)
-        selector = SelectorSyntax(elements, mark)
+        selector = SelectorSyntax(tests, mark)
         return selector
 
 

src/htsql/tr/syntax.py

 
 class FunctionOperatorSyntax(CallSyntax):
     """
-    Represents a binary function call in the operator form.
+    Represents a function call in the operator form.
 
-    This expression has the form::
+    This expression has one of the forms::
 
-        larg identifier rarg
+        arg1 :identifier
+        arg1 :identifier arg2
+        arg1 :identifier (arg2, ...)
 
     and is equivalent to the expression::
 
-        identifier(larg, rarg)
+        identifier(arg1, arg2, ...)
 
     `identifier` (:class:`IdentifierSyntax`)
         The function name.
 
-    `left_argument` (:class:`Syntax`)
-        The first argument.
-
-    `right_argument` (:class:`Syntax`)
-        The second argument.
+    `arguments` (:class:`Syntax`)
+        The function arguments.
     """
 
-    def __init__(self, identifier, left_argument, right_argument, mark):
+    def __init__(self, identifier, arguments, mark):
         assert isinstance(identifier, IdentifierSyntax)
-        # Note: the grammar may be changed to make the right argument optional.
-        assert isinstance(left_argument, Syntax)
-        assert isinstance(right_argument, Syntax)
+        assert isinstance(arguments, listof(Syntax))
+        assert len(arguments) > 0
 
         name = identifier.value
-        arguments = [left_argument, right_argument]
         super(FunctionOperatorSyntax, self).__init__(name, arguments, mark)
         self.identifier = identifier
-        self.left_argument = left_argument
-        self.right_argument = right_argument
 
     def __str__(self):
         # Generate an expression of the form:
-        #   larg identifier rarg
+        #   arg1:identifier(arg2,...)
         chunks = []
-        if self.left_argument is not None:
-            chunks.append(str(self.left_argument))
-            chunks.append(' ')
-        chunks.append(str(self.identifier))
-        if self.right_argument is not None:
-            chunks.append(' ')
-            chunks.append(str(self.right_argument))
+        chunks.append(str(self.arguments[0]))
+        chunks.append(':%s' % self.identifier)
+        if len(self.arguments) > 1:
+            chunks.append('(%s)' % ','.join(str(argument)
+                                            for argument in self.arguments[1:]))
         return ''.join(chunks)
 
 

test/output/pgsql.yaml

                                     (1 row)
 
          ----
-         /{null() as Title,null() as 'Title with whitespaces'}
+         /{null():as(Title),null():as('Title with whitespaces')}
          SELECT NULL, NULL
     - uri: /{null() as 'Hidden title' as 'Visible title'}
       status: 200 OK
                    (1 row)
 
          ----
-         /{null() as 'Hidden title' as 'Visible title'}
+         /{null():as('Hidden title'):as('Visible title')}
          SELECT NULL
     - uri: /{('HT' as HT)+('SQL' as SQL)}
       status: 200 OK
       headers:
       - [Content-Type, text/plain; charset=UTF-8]
       body: |2
-         |                             |
-        -+-----------------------------+-
-         | ('HT' as HT)+('SQL' as SQL) |
-        -+-----------------------------+-
-         | HTSQL                       |
-                                 (1 row)
-
-         ----
-         /{('HT' as HT)+('SQL' as SQL)}
+         |                               |
+        -+-------------------------------+-
+         | ('HT':as(HT))+('SQL':as(SQL)) |
+        -+-------------------------------+-
+         | HTSQL                         |
+                                   (1 row)
+
+         ----
+         /{('HT':as(HT))+('SQL':as(SQL))}
          SELECT ('HT' || 'SQL')
     - uri: /(school as Schools)
       status: 200 OK
                                             (9 rows)
 
          ----
-         /(school as Schools)
+         /(school:as(Schools))
          SELECT "school"."code", "school"."name" FROM "ad"."school" AS "school" ORDER BY 1 ASC
     - uri: /(school as Schools){name as Title}?code='art'
       status: 200 OK
                               (1 row)
 
          ----
-         /(school as Schools){name as Title}?code='art'
+         /(school:as(Schools)){name:as(Title)}?code='art'
          SELECT "school"."name" FROM "ad"."school" AS "school" WHERE ("school"."code" = 'art') ORDER BY "school"."code" ASC
     - uri: /school{* as Columns}
       status: 400 Bad Request
                                      (9 rows)
 
          ----
-         /school{name as Title+}
+         /school{name:as(Title)+}
          SELECT "school"."name" FROM "ad"."school" AS "school" ORDER BY 1 ASC, "school"."code" ASC
     - uri: /school{name+ as Title}
       status: 400 Bad Request
                                      (9 rows)
 
          ----
-         /school{(name+) as Title}
+         /school{(name+):as(Title)}
          SELECT "school"."name" FROM "ad"."school" AS "school" ORDER BY 1 ASC, "school"."code" ASC
     - uri: /course{department+,title,credits-}?number<200
       status: 200 OK
       status: 200 OK
       headers:
       - [Content-Type, text/csv; charset=UTF-8]
-      - [Content-Disposition, 'attachment; filename="((school as ''School Record''){code
-          as ''Code name'',name as ''Long name''}).csv"']
+      - [Content-Disposition, 'attachment; filename="((school:as(''School Record'')){code:as(''Code
+          name''),name:as(''Long name'')}).csv"']
       body: "Code name,Long name\r\nart,School of Art and Design\r\nbus,School of
         Business\r\nedu,College of Education\r\negn,School of Engineering\r\nla,\"School
         of Arts, Letters, and the Humanities\"\r\nmart,School of Modern Art\r\nmus,Musical
                                                  (9 rows)
 
          ----
-         /(school as 'School Record'){code as 'Code name',name as 'Long name'}
+         /(school:as('School Record')){code:as('Code name'),name:as('Long name')}
          SELECT "school"."code", "school"."name" FROM "ad"."school" AS "school" ORDER BY 1 ASC
     - uri: /(school as 'School Record'){code as 'Code name', name as 'Long name'}
       status: 200 OK
         <html>
         <head>
         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-        <title>/(school as 'School Record'){code as 'Code name',name as 'Long name'}</title>
+        <title>/(school:as('School Record')){code:as('Code name'),name:as('Long name')}</title>
         <style type="text/css">
         body { font-family: sans-serif; font-size: 90%; color: #515151; background: #ffffff }
         a:link, a:visited { color: #1f4884; text-decoration: none }
         </style>
         </head>
         <body>
-        <table class="page" summary="/(school as 'School Record'){code as 'Code name',name as 'Long name'}">
+        <table class="page" summary="/(school:as('School Record')){code:as('Code name'),name:as('Long name')}">
         <tr>
         <td class="content">
         <table class="chart" summary="School Record">
         <tr class="odd"><td>sc</td><td>School of Continuing Studies</td></tr>
         <tr class="total"><td colspan="2">(9 rows)</td></tr></table></td>
         </tr>
-        <tr><td class="footer">/(school as 'School Record'){code as 'Code name',name as 'Long name'}</td></tr>
+        <tr><td class="footer">/(school:as('School Record')){code:as('Code name'),name:as('Long name')}</td></tr>
         </table>
         </body>
         </html>
                                                                 (100 rows)
 
          ----
-         /course{department as 'Dept Code'+,number as 'No.',credits-,title}
+         /course{department:as('Dept Code')+,number:as('No.'),credits-,title}
          SELECT "course"."department", "course"."number", "course"."credits", "course"."title" FROM "ad"."course" AS "course" ORDER BY 1 ASC, 3 DESC, 2 ASC
     - uri: /program{school.name, title}
       status: 200 OK
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.