From fd595ba52c151793f4f36a9dbcdafbd4e9afa1ba Mon Sep 17 00:00:00 2001
From: Sander Mertens <sander.mertens8@gmail.com>
Date: Tue, 10 Oct 2023 15:45:06 -0700
Subject: [PATCH] Add query DSL feature that expands R(S, T, Q) into R(S, T),
 R(T, Q)

---
 flecs.c                       |  88 ++++++++++++++----
 flecs.h                       |   7 +-
 include/flecs/addons/parser.h |   7 +-
 src/addons/parser.c           |  43 +++++++--
 src/addons/plecs.c            |   2 +-
 src/entity.c                  |   4 +-
 src/filter.c                  |  39 ++++++--
 test/addons/project.json      |   7 +-
 test/addons/src/Parser.c      | 166 ++++++++++++++++++++++++++++++++++
 test/addons/src/main.c        |  27 +++++-
 10 files changed, 350 insertions(+), 40 deletions(-)

diff --git a/flecs.c b/flecs.c
index 0d748e1283..8a78c18177 100644
--- a/flecs.c
+++ b/flecs.c
@@ -4746,7 +4746,7 @@ ecs_table_t *flecs_traverse_from_expr(
     const char *ptr = expr;
     if (ptr) {
         ecs_term_t term = {0};
-        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
+        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term, NULL))){
             if (!ecs_term_is_initialized(&term)) {
                 break;
             }
@@ -4809,7 +4809,7 @@ void flecs_defer_from_expr(
     const char *ptr = expr;
     if (ptr) {
         ecs_term_t term = {0};
-        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
+        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term, NULL))) {
             if (!ecs_term_is_initialized(&term)) {
                 break;
             }
@@ -11053,23 +11053,50 @@ ecs_filter_t* ecs_filter_init(
         const char *name = NULL;
         const char *ptr = desc->expr;
         ecs_term_t term = {0};
+        ecs_term_id_t extra_args[ECS_PARSER_MAX_ARGS];
         int32_t expr_size = 0;
 
+        ecs_os_zeromem(extra_args);
+
         if (entity) {
             name = ecs_get_name(world, entity);
         }
 
-        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
+        while (ptr[0] && 
+            (ptr = ecs_parse_term(world, name, expr, ptr, &term, extra_args)))
+        {
             if (!ecs_term_is_initialized(&term)) {
                 break;
             }
 
-            if (expr_count == expr_size) {
-                expr_size = expr_size ? expr_size * 2 : 8;
-                expr_terms = ecs_os_realloc_n(expr_terms, ecs_term_t, expr_size);
-            }
+            int32_t arg = 0;
+
+            do {
+                ecs_assert(arg <= ECS_PARSER_MAX_ARGS, ECS_INTERNAL_ERROR, NULL);
+
+                if (expr_count == expr_size) {
+                    expr_size = expr_size ? expr_size * 2 : 8;
+                    expr_terms = ecs_os_realloc_n(expr_terms, ecs_term_t, expr_size);
+                }
+
+                ecs_term_t *expr_term = &expr_terms[expr_count ++];
+                *expr_term = term;
+
+                if (arg) {
+                    expr_term->src = expr_term[-1].second;
+                    expr_term->second = extra_args[arg - 1];
+
+                    if (expr_term->first.name != NULL) {
+                        expr_term->first.name = ecs_os_strdup(
+                            expr_term->first.name);
+                    }
+                    if (expr_term->src.name != NULL) {
+                        expr_term->src.name = ecs_os_strdup(
+                            expr_term->src.name);
+                    }
+                }
+            } while (ecs_term_id_is_set(&extra_args[arg ++]));
 
-            expr_terms[expr_count ++] = term;
             if (ptr[0] == '\n') {
                 break;
             }
@@ -30577,15 +30604,24 @@ const char* flecs_parse_arguments(
     int64_t column,
     const char *ptr,
     char *token,
-    ecs_term_t *term)
+    ecs_term_t *term,
+    ecs_term_id_t *extra_args)
 {
     (void)column;
 
     int32_t arg = 0;
 
+    if (extra_args) {
+        ecs_os_memset_n(extra_args, 0, ecs_term_id_t, ECS_PARSER_MAX_ARGS);
+    }
+
+    if (!term) {
+        arg = 2;
+    }
+
     do {
         if (flecs_valid_token_start_char(ptr[0])) {
-            if (arg == 2) {
+            if ((arg == ECS_PARSER_MAX_ARGS) || (!extra_args && arg == 2)) {
                 ecs_parser_error(name, expr, (ptr - expr), 
                     "too many arguments in term");
                 return NULL;
@@ -30602,6 +30638,8 @@ const char* flecs_parse_arguments(
                 term_id = &term->src;
             } else if (arg == 1) {
                 term_id = &term->second;
+            } else {
+                term_id = &extra_args[arg - 2];
             }
 
             /* If token is a colon, the token is an identifier followed by a
@@ -30643,7 +30681,9 @@ const char* flecs_parse_arguments(
             if (ptr[0] == TOK_AND) {
                 ptr = ecs_parse_ws(ptr + 1);
 
-                term->id_flags = ECS_PAIR;
+                if (term) {
+                    term->id_flags = ECS_PAIR;
+                }
 
             } else if (ptr[0] == TOK_PAREN_CLOSE) {
                 ptr = ecs_parse_ws(ptr + 1);
@@ -30689,7 +30729,8 @@ const char* flecs_parse_term(
     const ecs_world_t *world,
     const char *name,
     const char *expr,
-    ecs_term_t *term_out)
+    ecs_term_t *term_out,
+    ecs_term_id_t *extra_args)
 {
     const char *ptr = expr;
     char token[ECS_MAX_TOKEN_SIZE] = {0};
@@ -30837,7 +30878,7 @@ const char* flecs_parse_term(
             ptr = ecs_parse_ws(ptr);
         } else {
             ptr = flecs_parse_arguments(
-                world, name, expr, (ptr - expr), ptr, token, &term);
+                world, name, expr, (ptr - expr), ptr, token, &term, extra_args);
         }
 
         goto parse_done;
@@ -30955,8 +30996,7 @@ const char* flecs_parse_term(
             }
         }
 
-        if (ptr[0] == TOK_PAREN_CLOSE) {
-            ptr ++;
+        if (ptr[0] == TOK_PAREN_CLOSE || ptr[0] == TOK_AND) {
             goto parse_pair_object;
         } else {
             flecs_parser_unexpected_char(name, expr, ptr, ptr[0]);
@@ -30983,6 +31023,17 @@ const char* flecs_parse_term(
         term.id_flags = ECS_PAIR;
     }
 
+    if (ptr[0] == TOK_AND) {
+        ptr = ecs_parse_ws(ptr + 1);
+        ptr = flecs_parse_arguments(
+            world, name, expr, (ptr - expr), ptr, token, NULL, extra_args);
+        if (!ptr) {
+            goto error;
+        }
+    } else {
+        ptr ++;
+    }
+
     ptr = ecs_parse_ws(ptr);
     goto parse_done;
 
@@ -31020,7 +31071,8 @@ char* ecs_parse_term(
     const char *name,
     const char *expr,
     const char *ptr,
-    ecs_term_t *term)
+    ecs_term_t *term,
+    ecs_term_id_t *extra_args)
 {
     ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
     ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
@@ -31056,7 +31108,7 @@ char* ecs_parse_term(
     }
 
     /* Parse next element */
-    ptr = flecs_parse_term(world, name, ptr, term);
+    ptr = flecs_parse_term(world, name, ptr, term, extra_args);
     if (!ptr) {
         goto error;
     }
@@ -32887,7 +32939,7 @@ const char *plecs_parse_plecs_term(
         decl_id = state->last_predicate;
     }
 
-    ptr = ecs_parse_term(world, name, expr, ptr, &term);
+    ptr = ecs_parse_term(world, name, expr, ptr, &term, NULL);
     if (!ptr) {
         return NULL;
     }
diff --git a/flecs.h b/flecs.h
index 8aebdfbb6f..4b03f41731 100644
--- a/flecs.h
+++ b/flecs.h
@@ -15267,6 +15267,9 @@ void ecs_snapshot_free(
 #ifndef FLECS_PARSER_H
 #define FLECS_PARSER_H
 
+/** Maximum number of extra arguments in term expression */
+#define ECS_PARSER_MAX_ARGS (16)
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -15353,6 +15356,7 @@ const char* ecs_parse_token(
  * @param expr The expression to parse (optional, improves error logs)
  * @param ptr The pointer to the current term (must be in expr).
  * @param term_out Out parameter for the term.
+ * @param extra_args Out array for extra args, must be of size ECS_PARSER_MAX_ARGS.
  * @return pointer to next term if successful, NULL if failed.
  */
 FLECS_API
@@ -15361,7 +15365,8 @@ char* ecs_parse_term(
     const char *name,
     const char *expr,
     const char *ptr,
-    ecs_term_t *term_out);
+    ecs_term_t *term_out,
+    ecs_term_id_t *extra_args);
 
 #ifdef __cplusplus
 }
diff --git a/include/flecs/addons/parser.h b/include/flecs/addons/parser.h
index 890afb7d52..b4a8d7c693 100644
--- a/include/flecs/addons/parser.h
+++ b/include/flecs/addons/parser.h
@@ -19,6 +19,9 @@
 #ifndef FLECS_PARSER_H
 #define FLECS_PARSER_H
 
+/** Maximum number of extra arguments in term expression */
+#define ECS_PARSER_MAX_ARGS (16)
+
 #ifdef __cplusplus
 extern "C" {
 #endif
@@ -105,6 +108,7 @@ const char* ecs_parse_token(
  * @param expr The expression to parse (optional, improves error logs)
  * @param ptr The pointer to the current term (must be in expr).
  * @param term_out Out parameter for the term.
+ * @param extra_args Out array for extra args, must be of size ECS_PARSER_MAX_ARGS.
  * @return pointer to next term if successful, NULL if failed.
  */
 FLECS_API
@@ -113,7 +117,8 @@ char* ecs_parse_term(
     const char *name,
     const char *expr,
     const char *ptr,
-    ecs_term_t *term_out);
+    ecs_term_t *term_out,
+    ecs_term_id_t *extra_args);
 
 #ifdef __cplusplus
 }
diff --git a/src/addons/parser.c b/src/addons/parser.c
index 8e02040903..98c68101cd 100644
--- a/src/addons/parser.c
+++ b/src/addons/parser.c
@@ -487,15 +487,24 @@ const char* flecs_parse_arguments(
     int64_t column,
     const char *ptr,
     char *token,
-    ecs_term_t *term)
+    ecs_term_t *term,
+    ecs_term_id_t *extra_args)
 {
     (void)column;
 
     int32_t arg = 0;
 
+    if (extra_args) {
+        ecs_os_memset_n(extra_args, 0, ecs_term_id_t, ECS_PARSER_MAX_ARGS);
+    }
+
+    if (!term) {
+        arg = 2;
+    }
+
     do {
         if (flecs_valid_token_start_char(ptr[0])) {
-            if (arg == 2) {
+            if ((arg == ECS_PARSER_MAX_ARGS) || (!extra_args && arg == 2)) {
                 ecs_parser_error(name, expr, (ptr - expr), 
                     "too many arguments in term");
                 return NULL;
@@ -512,6 +521,8 @@ const char* flecs_parse_arguments(
                 term_id = &term->src;
             } else if (arg == 1) {
                 term_id = &term->second;
+            } else {
+                term_id = &extra_args[arg - 2];
             }
 
             /* If token is a colon, the token is an identifier followed by a
@@ -553,7 +564,9 @@ const char* flecs_parse_arguments(
             if (ptr[0] == TOK_AND) {
                 ptr = ecs_parse_ws(ptr + 1);
 
-                term->id_flags = ECS_PAIR;
+                if (term) {
+                    term->id_flags = ECS_PAIR;
+                }
 
             } else if (ptr[0] == TOK_PAREN_CLOSE) {
                 ptr = ecs_parse_ws(ptr + 1);
@@ -599,7 +612,8 @@ const char* flecs_parse_term(
     const ecs_world_t *world,
     const char *name,
     const char *expr,
-    ecs_term_t *term_out)
+    ecs_term_t *term_out,
+    ecs_term_id_t *extra_args)
 {
     const char *ptr = expr;
     char token[ECS_MAX_TOKEN_SIZE] = {0};
@@ -747,7 +761,7 @@ const char* flecs_parse_term(
             ptr = ecs_parse_ws(ptr);
         } else {
             ptr = flecs_parse_arguments(
-                world, name, expr, (ptr - expr), ptr, token, &term);
+                world, name, expr, (ptr - expr), ptr, token, &term, extra_args);
         }
 
         goto parse_done;
@@ -865,8 +879,7 @@ const char* flecs_parse_term(
             }
         }
 
-        if (ptr[0] == TOK_PAREN_CLOSE) {
-            ptr ++;
+        if (ptr[0] == TOK_PAREN_CLOSE || ptr[0] == TOK_AND) {
             goto parse_pair_object;
         } else {
             flecs_parser_unexpected_char(name, expr, ptr, ptr[0]);
@@ -893,6 +906,17 @@ const char* flecs_parse_term(
         term.id_flags = ECS_PAIR;
     }
 
+    if (ptr[0] == TOK_AND) {
+        ptr = ecs_parse_ws(ptr + 1);
+        ptr = flecs_parse_arguments(
+            world, name, expr, (ptr - expr), ptr, token, NULL, extra_args);
+        if (!ptr) {
+            goto error;
+        }
+    } else {
+        ptr ++;
+    }
+
     ptr = ecs_parse_ws(ptr);
     goto parse_done;
 
@@ -930,7 +954,8 @@ char* ecs_parse_term(
     const char *name,
     const char *expr,
     const char *ptr,
-    ecs_term_t *term)
+    ecs_term_t *term,
+    ecs_term_id_t *extra_args)
 {
     ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
     ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
@@ -966,7 +991,7 @@ char* ecs_parse_term(
     }
 
     /* Parse next element */
-    ptr = flecs_parse_term(world, name, ptr, term);
+    ptr = flecs_parse_term(world, name, ptr, term, extra_args);
     if (!ptr) {
         goto error;
     }
diff --git a/src/addons/plecs.c b/src/addons/plecs.c
index a426701d11..856135122a 100644
--- a/src/addons/plecs.c
+++ b/src/addons/plecs.c
@@ -1711,7 +1711,7 @@ const char *plecs_parse_plecs_term(
         decl_id = state->last_predicate;
     }
 
-    ptr = ecs_parse_term(world, name, expr, ptr, &term);
+    ptr = ecs_parse_term(world, name, expr, ptr, &term, NULL);
     if (!ptr) {
         return NULL;
     }
diff --git a/src/entity.c b/src/entity.c
index ed32041b47..088d0b5ceb 100644
--- a/src/entity.c
+++ b/src/entity.c
@@ -1377,7 +1377,7 @@ ecs_table_t *flecs_traverse_from_expr(
     const char *ptr = expr;
     if (ptr) {
         ecs_term_t term = {0};
-        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
+        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term, NULL))){
             if (!ecs_term_is_initialized(&term)) {
                 break;
             }
@@ -1440,7 +1440,7 @@ void flecs_defer_from_expr(
     const char *ptr = expr;
     if (ptr) {
         ecs_term_t term = {0};
-        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
+        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term, NULL))) {
             if (!ecs_term_is_initialized(&term)) {
                 break;
             }
diff --git a/src/filter.c b/src/filter.c
index 0ef4c2aae6..8f170589ec 100644
--- a/src/filter.c
+++ b/src/filter.c
@@ -1432,23 +1432,50 @@ ecs_filter_t* ecs_filter_init(
         const char *name = NULL;
         const char *ptr = desc->expr;
         ecs_term_t term = {0};
+        ecs_term_id_t extra_args[ECS_PARSER_MAX_ARGS];
         int32_t expr_size = 0;
 
+        ecs_os_zeromem(extra_args);
+
         if (entity) {
             name = ecs_get_name(world, entity);
         }
 
-        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
+        while (ptr[0] && 
+            (ptr = ecs_parse_term(world, name, expr, ptr, &term, extra_args)))
+        {
             if (!ecs_term_is_initialized(&term)) {
                 break;
             }
 
-            if (expr_count == expr_size) {
-                expr_size = expr_size ? expr_size * 2 : 8;
-                expr_terms = ecs_os_realloc_n(expr_terms, ecs_term_t, expr_size);
-            }
+            int32_t arg = 0;
+
+            do {
+                ecs_assert(arg <= ECS_PARSER_MAX_ARGS, ECS_INTERNAL_ERROR, NULL);
+
+                if (expr_count == expr_size) {
+                    expr_size = expr_size ? expr_size * 2 : 8;
+                    expr_terms = ecs_os_realloc_n(expr_terms, ecs_term_t, expr_size);
+                }
+
+                ecs_term_t *expr_term = &expr_terms[expr_count ++];
+                *expr_term = term;
+
+                if (arg) {
+                    expr_term->src = expr_term[-1].second;
+                    expr_term->second = extra_args[arg - 1];
+
+                    if (expr_term->first.name != NULL) {
+                        expr_term->first.name = ecs_os_strdup(
+                            expr_term->first.name);
+                    }
+                    if (expr_term->src.name != NULL) {
+                        expr_term->src.name = ecs_os_strdup(
+                            expr_term->src.name);
+                    }
+                }
+            } while (ecs_term_id_is_set(&extra_args[arg ++]));
 
-            expr_terms[expr_count ++] = term;
             if (ptr[0] == '\n') {
                 break;
             }
diff --git a/test/addons/project.json b/test/addons/project.json
index 6139d980eb..93d6fa3b0b 100644
--- a/test/addons/project.json
+++ b/test/addons/project.json
@@ -238,7 +238,12 @@
                 "query_not_scope",
                 "query_empty_scope",
                 "override_tag",
-                "override_pair"
+                "override_pair",
+                "pair_3_args",
+                "pair_3_args_implicit_this",
+                "pair_4_args",
+                "pair_4_args_implicit_this",
+                "pair_3_args_2_terms"
             ]
         }, {
             "id": "Plecs",
diff --git a/test/addons/src/Parser.c b/test/addons/src/Parser.c
index eb015c21b5..149b0632aa 100644
--- a/test/addons/src/Parser.c
+++ b/test/addons/src/Parser.c
@@ -5339,3 +5339,169 @@ void Parser_override_pair(void) {
 
     ecs_fini(world);
 }
+
+void Parser_pair_3_args(void) {
+    ecs_world_t *world = ecs_mini();
+
+    ecs_filter_t f = ECS_FILTER_INIT;
+    test_assert(NULL != ecs_filter_init(world, &(ecs_filter_desc_t){
+        .storage = &f,
+        .expr = "ChildOf($this, $parent, $grandparent)"
+    }));
+
+    test_int(filter_count(&f), 2);
+
+    ecs_term_t *terms = filter_terms(&f);
+    test_first(terms[0], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src(terms[0], EcsThis, EcsSelf|EcsIsVariable);
+    test_second_var(terms[0], 0, EcsSelf|EcsIsVariable, "parent");
+    test_int(terms[0].oper, EcsAnd);
+    test_int(terms[0].inout, EcsInOutDefault);
+
+    test_first(terms[1], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[1], 0, EcsSelf|EcsIsVariable, "parent");
+    test_second_var(terms[1], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_int(terms[1].oper, EcsAnd);
+    test_int(terms[1].inout, EcsInOutDefault);
+
+    ecs_filter_fini(&f);
+
+    ecs_fini(world);
+}
+
+void Parser_pair_3_args_implicit_this(void) {
+    ecs_world_t *world = ecs_mini();
+
+    ecs_filter_t f = ECS_FILTER_INIT;
+    test_assert(NULL != ecs_filter_init(world, &(ecs_filter_desc_t){
+        .storage = &f,
+        .expr = "(ChildOf, $parent, $grandparent)"
+    }));
+
+    test_int(filter_count(&f), 2);
+
+    ecs_term_t *terms = filter_terms(&f);
+    test_first(terms[0], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src(terms[0], EcsThis, EcsSelf|EcsIsVariable);
+    test_second_var(terms[0], 0, EcsSelf|EcsIsVariable, "parent");
+    test_int(terms[0].oper, EcsAnd);
+    test_int(terms[0].inout, EcsInOutDefault);
+
+    test_first(terms[1], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[1], 0, EcsSelf|EcsIsVariable, "parent");
+    test_second_var(terms[1], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_int(terms[1].oper, EcsAnd);
+    test_int(terms[1].inout, EcsInOutDefault);
+
+    ecs_filter_fini(&f);
+
+    ecs_fini(world);
+}
+
+void Parser_pair_4_args(void) {
+    ecs_world_t *world = ecs_mini();
+
+    ecs_filter_t f = ECS_FILTER_INIT;
+    test_assert(NULL != ecs_filter_init(world, &(ecs_filter_desc_t){
+        .storage = &f,
+        .expr = "ChildOf($this, $parent, $grandparent, $greatgrandparent)"
+    }));
+
+    test_int(filter_count(&f), 3);
+
+    ecs_term_t *terms = filter_terms(&f);
+    test_first(terms[0], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src(terms[0], EcsThis, EcsSelf|EcsIsVariable);
+    test_second_var(terms[0], 0, EcsSelf|EcsIsVariable, "parent");
+    test_int(terms[0].oper, EcsAnd);
+    test_int(terms[0].inout, EcsInOutDefault);
+
+    test_first(terms[1], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[1], 0, EcsSelf|EcsIsVariable, "parent");
+    test_second_var(terms[1], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_int(terms[1].oper, EcsAnd);
+    test_int(terms[1].inout, EcsInOutDefault);
+
+    test_first(terms[2], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[2], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_second_var(terms[2], 0, EcsSelf|EcsIsVariable, "greatgrandparent");
+    test_int(terms[2].oper, EcsAnd);
+    test_int(terms[2].inout, EcsInOutDefault);
+
+    ecs_filter_fini(&f);
+
+    ecs_fini(world);
+}
+
+void Parser_pair_4_args_implicit_this(void) {
+    ecs_world_t *world = ecs_mini();
+
+    ecs_filter_t f = ECS_FILTER_INIT;
+    test_assert(NULL != ecs_filter_init(world, &(ecs_filter_desc_t){
+        .storage = &f,
+        .expr = "(ChildOf, $parent, $grandparent, $greatgrandparent)"
+    }));
+
+    test_int(filter_count(&f), 3);
+
+    ecs_term_t *terms = filter_terms(&f);
+    test_first(terms[0], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src(terms[0], EcsThis, EcsSelf|EcsIsVariable);
+    test_second_var(terms[0], 0, EcsSelf|EcsIsVariable, "parent");
+    test_int(terms[0].oper, EcsAnd);
+    test_int(terms[0].inout, EcsInOutDefault);
+
+    test_first(terms[1], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[1], 0, EcsSelf|EcsIsVariable, "parent");
+    test_second_var(terms[1], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_int(terms[1].oper, EcsAnd);
+    test_int(terms[1].inout, EcsInOutDefault);
+
+    test_first(terms[2], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[2], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_second_var(terms[2], 0, EcsSelf|EcsIsVariable, "greatgrandparent");
+    test_int(terms[2].oper, EcsAnd);
+    test_int(terms[2].inout, EcsInOutDefault);
+
+    ecs_filter_fini(&f);
+
+    ecs_fini(world);
+}
+
+void Parser_pair_3_args_2_terms(void) {
+    ecs_world_t *world = ecs_mini();
+
+    ECS_TAG(world, Rel);
+    ECS_TAG(world, Tgt);
+
+    ecs_filter_t f = ECS_FILTER_INIT;
+    test_assert(NULL != ecs_filter_init(world, &(ecs_filter_desc_t){
+        .storage = &f,
+        .expr = "ChildOf($this, $parent, $grandparent), Rel($this, $parent)"
+    }));
+
+    test_int(filter_count(&f), 3);
+
+    ecs_term_t *terms = filter_terms(&f);
+    test_first(terms[0], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src(terms[0], EcsThis, EcsSelf|EcsIsVariable);
+    test_second_var(terms[0], 0, EcsSelf|EcsIsVariable, "parent");
+    test_int(terms[0].oper, EcsAnd);
+    test_int(terms[0].inout, EcsInOutDefault);
+
+    test_first(terms[1], EcsChildOf, EcsSelf|EcsIsEntity);
+    test_src_var(terms[1], 0, EcsSelf|EcsIsVariable, "parent");
+    test_second_var(terms[1], 0, EcsSelf|EcsIsVariable, "grandparent");
+    test_int(terms[1].oper, EcsAnd);
+    test_int(terms[1].inout, EcsInOutDefault);
+
+    test_first(terms[2], Rel, EcsSelf|EcsIsEntity);
+    test_src(terms[2], EcsThis, EcsSelf|EcsUp|EcsIsVariable);
+    test_second_var(terms[2], 0, EcsSelf|EcsIsVariable, "parent");
+    test_int(terms[2].oper, EcsAnd);
+    test_int(terms[2].inout, EcsInOutDefault);
+
+    ecs_filter_fini(&f);
+
+    ecs_fini(world);
+}
diff --git a/test/addons/src/main.c b/test/addons/src/main.c
index ba29f30d5b..a7dd42d1a4 100644
--- a/test/addons/src/main.c
+++ b/test/addons/src/main.c
@@ -234,6 +234,11 @@ void Parser_query_not_scope(void);
 void Parser_query_empty_scope(void);
 void Parser_override_tag(void);
 void Parser_override_pair(void);
+void Parser_pair_3_args(void);
+void Parser_pair_3_args_implicit_this(void);
+void Parser_pair_4_args(void);
+void Parser_pair_4_args_implicit_this(void);
+void Parser_pair_3_args_2_terms(void);
 
 // Testsuite 'Plecs'
 void Plecs_null(void);
@@ -2453,6 +2458,26 @@ bake_test_case Parser_testcases[] = {
     {
         "override_pair",
         Parser_override_pair
+    },
+    {
+        "pair_3_args",
+        Parser_pair_3_args
+    },
+    {
+        "pair_3_args_implicit_this",
+        Parser_pair_3_args_implicit_this
+    },
+    {
+        "pair_4_args",
+        Parser_pair_4_args
+    },
+    {
+        "pair_4_args_implicit_this",
+        Parser_pair_4_args_implicit_this
+    },
+    {
+        "pair_3_args_2_terms",
+        Parser_pair_3_args_2_terms
     }
 };
 
@@ -7531,7 +7556,7 @@ static bake_test_suite suites[] = {
         "Parser",
         NULL,
         NULL,
-        225,
+        230,
         Parser_testcases
     },
     {