@@ -759,6 +759,13 @@ static void expr_free(cbm_expr_t *e) {
759759 safe_str_free (& cur -> cond .in_values [i ]);
760760 }
761761 free (cur -> cond .in_values );
762+ safe_str_free (& cur -> cond .func );
763+ for (int i = 0 ; i < cur -> cond .arg_count ; i ++ ) {
764+ safe_str_free (& cur -> cond .args [i ].variable );
765+ safe_str_free (& cur -> cond .args [i ].property );
766+ safe_str_free (& cur -> cond .args [i ].literal );
767+ }
768+ free (cur -> cond .args );
762769 }
763770 if (cur -> right ) {
764771 if (top < EXPR_FREE_STACK ) {
@@ -837,6 +844,37 @@ static const char *unsupported_clause_error(cbm_token_type_t type) {
837844
838845/* Forward declarations for recursive descent */
839846static cbm_expr_t * parse_or_expr (parser_t * p );
847+ /* Multi-arg scalar function support, shared with the RETURN-item parser (#874) */
848+ static bool is_multiarg_func_call (parser_t * p );
849+ static int parse_multiarg_func_item (parser_t * p , cbm_return_item_t * item );
850+
851+ /* Free a multi-arg function argument array. */
852+ static void func_args_free (cbm_func_arg_t * args , int count ) {
853+ for (int i = 0 ; i < count ; i ++ ) {
854+ safe_str_free (& args [i ].variable );
855+ safe_str_free (& args [i ].property );
856+ safe_str_free (& args [i ].literal );
857+ }
858+ free (args );
859+ }
860+
861+ /* Free the fields of a partially-parsed multi-arg function item. */
862+ static void func_item_fields_free (cbm_return_item_t * item ) {
863+ safe_str_free (& item -> variable );
864+ safe_str_free (& item -> property );
865+ safe_str_free (& item -> func );
866+ func_args_free (item -> args , item -> arg_count );
867+ item -> args = NULL ;
868+ item -> arg_count = 0 ;
869+ }
870+
871+ /* Free the function-call fields of a WHERE condition (#874). */
872+ static void cond_func_fields_free (cbm_condition_t * c ) {
873+ safe_str_free (& c -> func );
874+ func_args_free (c -> args , c -> arg_count );
875+ c -> args = NULL ;
876+ c -> arg_count = 0 ;
877+ }
840878
841879/* Parse IN [val, val, ...] list. Returns expr_leaf or NULL on error. */
842880static cbm_expr_t * parse_in_list (parser_t * p , cbm_condition_t * c ) {
@@ -1007,48 +1045,94 @@ static cbm_expr_t *parse_exists_predicate(parser_t *p, bool negated) {
10071045 return expr_leaf (c );
10081046}
10091047
1010- static cbm_expr_t * parse_condition_expr (parser_t * p ) {
1011- /* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */
1012- bool negated = match (p , TOK_NOT );
1048+ /* parse_condition_lhs result: the label-test form is a complete condition. */
1049+ enum { COND_LHS_COMPLETE = 1 };
1050+
1051+ /* Parse the left-hand side of a WHERE condition into c.
1052+ * Returns CBM_NOT_FOUND on error, 0 when an operator/value should follow, and
1053+ * COND_LHS_COMPLETE when the condition is already complete (label test). */
1054+ static int parse_condition_lhs (parser_t * p , cbm_condition_t * c ) {
1055+ if (is_multiarg_func_call (p )) {
1056+ /* Multi-arg scalar function LHS: coalesce(f.depth, 0) >= 2 (#874).
1057+ * Reuse the RETURN-item parser, then move ownership into the condition. */
1058+ cbm_return_item_t fitem ;
1059+ memset (& fitem , 0 , sizeof (fitem ));
1060+ if (parse_multiarg_func_item (p , & fitem ) < 0 ) {
1061+ func_item_fields_free (& fitem );
1062+ return CBM_NOT_FOUND ;
1063+ }
1064+ c -> variable = fitem .variable ;
1065+ c -> property = fitem .property ;
1066+ c -> func = fitem .func ;
1067+ c -> args = fitem .args ;
1068+ c -> arg_count = fitem .arg_count ;
1069+ return 0 ;
1070+ }
10131071
1014- /* EXISTS { pattern } predicate (anchored single-hop existence). */
1015- if (check (p , TOK_EXISTS )) {
1016- return parse_exists_predicate (p , negated );
1072+ if (check (p , TOK_IDENT ) && p -> pos + SKIP_ONE < p -> count &&
1073+ p -> tokens [p -> pos + SKIP_ONE ].type == TOK_LPAREN ) {
1074+ /* Unrecognised function call in WHERE — fail loudly with the supported
1075+ * set instead of the misleading "unexpected operator" (#874). */
1076+ snprintf (p -> error , sizeof (p -> error ),
1077+ "unsupported function '%s' in WHERE (supported: coalesce, substring, replace, "
1078+ "left, right)" ,
1079+ peek (p )-> text );
1080+ return CBM_NOT_FOUND ;
10171081 }
10181082
10191083 const cbm_token_t * var = expect (p , TOK_IDENT );
10201084 if (!var ) {
1021- return NULL ;
1085+ return CBM_NOT_FOUND ;
10221086 }
10231087
1024- cbm_condition_t c = {0 };
1025- c .negated = negated ;
1026-
10271088 /* Label test: WHERE n:Label (openCypher, #241). Modelled as a leaf with
10281089 * op="HAS_LABEL" and value=Label, evaluated against the bound node's label. */
10291090 if (check (p , TOK_COLON )) {
10301091 advance (p );
10311092 const cbm_token_t * lbl = expect (p , TOK_IDENT );
10321093 if (!lbl ) {
1033- return NULL ;
1094+ return CBM_NOT_FOUND ;
10341095 }
1035- c . variable = heap_strdup (var -> text );
1036- c . op = heap_strdup ("HAS_LABEL" );
1037- c . value = heap_strdup (lbl -> text );
1038- return expr_leaf ( c ) ;
1096+ c -> variable = heap_strdup (var -> text );
1097+ c -> op = heap_strdup ("HAS_LABEL" );
1098+ c -> value = heap_strdup (lbl -> text );
1099+ return COND_LHS_COMPLETE ;
10391100 }
10401101
10411102 if (match (p , TOK_DOT )) {
10421103 const cbm_token_t * prop = expect (p , TOK_IDENT );
10431104 if (!prop ) {
1044- return NULL ;
1105+ return CBM_NOT_FOUND ;
10451106 }
1046- c . variable = heap_strdup (var -> text );
1047- c . property = heap_strdup (prop -> text );
1107+ c -> variable = heap_strdup (var -> text );
1108+ c -> property = heap_strdup (prop -> text );
10481109 } else {
10491110 /* No dot: bare alias (e.g. post-WITH variable like "cnt") */
1050- c .variable = heap_strdup (var -> text );
1051- c .property = NULL ;
1111+ c -> variable = heap_strdup (var -> text );
1112+ c -> property = NULL ;
1113+ }
1114+ return 0 ;
1115+ }
1116+
1117+ static cbm_expr_t * parse_condition_expr (parser_t * p ) {
1118+ /* Check for NOT prefix at condition level (e.g. NOT n.name CONTAINS "x") */
1119+ bool negated = match (p , TOK_NOT );
1120+
1121+ /* EXISTS { pattern } predicate (anchored single-hop existence). */
1122+ if (check (p , TOK_EXISTS )) {
1123+ return parse_exists_predicate (p , negated );
1124+ }
1125+
1126+ cbm_condition_t c = {0 };
1127+ c .negated = negated ;
1128+
1129+ int lhs_rc = parse_condition_lhs (p , & c );
1130+ if (lhs_rc < 0 ) {
1131+ return NULL ;
1132+ }
1133+ if (lhs_rc > 0 ) {
1134+ /* HAS_LABEL leaf — complete condition, no operator follows */
1135+ return expr_leaf (c );
10521136 }
10531137
10541138 /* IS NULL / IS NOT NULL */
@@ -1066,13 +1150,19 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) {
10661150
10671151 /* IN [...] */
10681152 if (check (p , TOK_IN )) {
1069- return parse_in_list (p , & c );
1153+ cbm_expr_t * e = parse_in_list (p , & c );
1154+ if (!e ) {
1155+ /* parse_in_list frees variable/property/op; func fields are ours */
1156+ cond_func_fields_free (& c );
1157+ }
1158+ return e ;
10701159 }
10711160
10721161 /* Standard operators */
10731162 c .op = parse_comparison_op (p );
10741163 if (!c .op ) {
10751164 snprintf (p -> error , sizeof (p -> error ), "unexpected operator at pos %d" , peek (p )-> pos );
1165+ cond_func_fields_free (& c );
10761166 safe_str_free (& c .variable );
10771167 safe_str_free (& c .property );
10781168 return NULL ;
@@ -1089,6 +1179,7 @@ static cbm_expr_t *parse_condition_expr(parser_t *p) {
10891179 c .value = heap_strdup ("false" );
10901180 } else {
10911181 snprintf (p -> error , sizeof (p -> error ), "expected value at pos %d" , peek (p )-> pos );
1182+ cond_func_fields_free (& c );
10921183 safe_str_free (& c .variable );
10931184 safe_str_free (& c .property );
10941185 safe_str_free (& c .op );
@@ -1949,6 +2040,8 @@ static void free_where(cbm_where_clause_t *w) {
19492040 safe_str_free (& w -> conditions [i ].in_values [j ]);
19502041 }
19512042 free (w -> conditions [i ].in_values );
2043+ safe_str_free (& w -> conditions [i ].func );
2044+ func_args_free (w -> conditions [i ].args , w -> conditions [i ].arg_count );
19522045 }
19532046 free (w -> conditions );
19542047 safe_str_free (& w -> op );
@@ -2338,8 +2431,26 @@ static void binding_set(binding_t *b, const char *var, const cbm_node_t *node) {
23382431 b -> var_count ++ ;
23392432}
23402433
2434+ static const char * eval_multiarg_func (binding_t * b , const cbm_return_item_t * item , char * buf ,
2435+ size_t bufsz );
2436+
23412437/* Resolve the actual property value for a condition from a binding */
23422438static const char * resolve_condition_value (const cbm_condition_t * c , binding_t * b ) {
2439+ /* Multi-arg scalar function LHS: coalesce(f.depth, 0) >= 2 (#874).
2440+ * Evaluated through the same code path as RETURN projections. The value is
2441+ * consumed by eval_condition before any other condition is resolved, so a
2442+ * single thread-local buffer per call is safe. */
2443+ if (c -> func ) {
2444+ static _Thread_local char func_buf [CBM_SZ_512 ];
2445+ cbm_return_item_t item = {0 };
2446+ item .variable = c -> variable ;
2447+ item .property = c -> property ;
2448+ item .func = c -> func ;
2449+ item .args = c -> args ;
2450+ item .arg_count = c -> arg_count ;
2451+ return eval_multiarg_func (b , & item , func_buf , sizeof (func_buf ));
2452+ }
2453+
23432454 cbm_edge_t * e = binding_get_edge (b , c -> variable );
23442455 if (e ) {
23452456 return edge_prop (e , c -> property );
0 commit comments