File 9361-erts-Support-prefix-argument-matching-in-trace-match.patch of Package erlang

From cbdbfd54f34ff8de3f033b25367f3492c6e3ec97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Muska=C5=82a?= <micmus@whatsapp.com>
Date: Tue, 24 Feb 2026 02:05:19 -0800
Subject: [PATCH 1/2] erts: Support prefix argument matching in trace match
 specs

Allow trace match spec heads to use [Arg1, Arg2 | '_'] syntax to match
functions with at least N arguments, regardless of actual arity. This is
useful when combined with the '_' arity wildcard in trace:function/4, e.g.
to trace lists:seq/2 and lists:seq/3 with a single match spec that only
constrains the first argument.

The prefix flag is encoded as a tag bit on the arity operand of the
existing matchArray instruction (bit 8, since max arity is 255), avoiding
the need for a separate VM instruction.
---
 erts/doc/guides/match_spec.md           |  15 ++-
 erts/emulator/beam/erl_db_hash.c        |   2 +-
 erts/emulator/beam/erl_db_tree.c        |   2 +-
 erts/emulator/beam/erl_db_util.c        |  59 ++++++++++--
 erts/emulator/beam/erl_db_util.h        |   3 +-
 erts/emulator/test/call_trace_SUITE.erl | 117 +++++++++++++++++++++++-
 6 files changed, 183 insertions(+), 15 deletions(-)

diff --git a/erts/doc/src/match_spec.xml b/erts/doc/src/match_spec.xml
index 9f84119ba7..eb8e70c9b4 100644
--- a/erts/doc/src/match_spec.xml
+++ b/erts/doc/src/match_spec.xml
@@ -70,7 +70,7 @@
       <item>MatchFunction ::= { MatchHead, MatchConditions, MatchBody }
       </item>
       <item>MatchHead ::=  MatchVariable | <c><![CDATA['_']]></c> |
-        [ MatchHeadPart, ... ]
+        [ MatchHeadPart, ... ] | [ MatchHeadPart, ... | <c><![CDATA['_']]></c> ]
       </item>
       <item>MatchHeadPart ::= term() | MatchVariable | <c><![CDATA['_']]></c>
       </item>
@@ -872,6 +872,20 @@
   []},
  {'_',[],[]}]
     ]]></code>
+
+    <p>Match all calls with two or more arguments where the first argument is the atom
+     <c>'error'</c>, using a prefix match head (note the <c>| <![CDATA['_']]></c> tail):</p>
+
+    <code type="none"><![CDATA[
+[{['error', '_' | '_'],
+  [],
+  []}]
+    ]]></code>
+
+
+    <p>This is mainly useful when combined with a wildcard arity in the trace pattern,
+     for example <c>trace:function(S, {Mod, Fun, <![CDATA['_']]>}, MatchSpec, [])</c>, where the
+     match specification needs to match calls regardless of their arity.</p>
   </section>
 
   <section>
diff --git a/erts/emulator/beam/erl_db_hash.c b/erts/emulator/beam/erl_db_hash.c
index 9ae1389c60..f67bc35be3 100644
--- a/erts/emulator/beam/erl_db_hash.c
+++ b/erts/emulator/beam/erl_db_hash.c
@@ -3305,7 +3305,7 @@ static int analyze_pattern(DbTableHash *tb, Eterm pattern,
      */
     if ((mpi->mp = db_match_compile(matches, guards, bodies,
 				    num_heads, DCOMP_TABLE, NULL,
-                                    &freason)) 
+                                    &freason, NULL))
 	== NULL) {
 	if (buff != sbuff) { 
 	    erts_free(ERTS_ALC_T_DB_TMP, buff);
diff --git a/erts/emulator/beam/erl_db_tree.c b/erts/emulator/beam/erl_db_tree.c
index 53ea998bf8..c2d9c47bad 100644
--- a/erts/emulator/beam/erl_db_tree.c
+++ b/erts/emulator/beam/erl_db_tree.c
@@ -2857,7 +2857,7 @@ static int analyze_pattern(DbTableCommon *tb, Eterm pattern,
      */
     if ((mpi->mp = db_match_compile(matches, guards, bodies,
 				    num_heads, DCOMP_TABLE, NULL,
-                                    &freason)) 
+                                    &freason, NULL))
 	== NULL) {
 	if (buff != sbuff) { 
 	    erts_free(ERTS_ALC_T_DB_TMP, buff);
diff --git a/erts/emulator/beam/erl_db_util.c b/erts/emulator/beam/erl_db_util.c
index 081892a828..bdd32d3be5 100644
--- a/erts/emulator/beam/erl_db_util.c
+++ b/erts/emulator/beam/erl_db_util.c
@@ -275,6 +275,7 @@ set_match_trace(Process *tracee_p, Eterm fail_term, ErtsTracer tracer,
 typedef enum {
     matchArray, /* Only when parameter is an array (DCOMP_TRACE) */
     matchArrayBind, /* ------------- " ------------ */
+    matchArrayPrefix, /* Prefix match: arity >= n (DCOMP_TRACE) */
     matchTuple,
     matchPushT,
     matchPushL,
@@ -1223,6 +1224,8 @@ Binary *db_match_set_compile(Process *p, Eterm matchexpr,
     Eterm *matches,*guards, *bodies;
     Eterm *buff;
     Eterm sbuff[15];
+    bool *prefix_flags = NULL;
+    bool sprefix[5] = {0};
 
     *freasonp = BADARG;
 
@@ -1238,8 +1241,11 @@ Binary *db_match_set_compile(Process *p, Eterm matchexpr,
     if (num_heads > 5) {
 	buff = erts_alloc(ERTS_ALC_T_DB_TMP,
 			  sizeof(Eterm) * num_heads * 3);
+	prefix_flags = erts_alloc(ERTS_ALC_T_DB_TMP,
+				  sizeof(bool) * num_heads);
     } else {
 	buff = sbuff;
+	prefix_flags = sprefix;
     }
 
     matches = buff;
@@ -1252,6 +1258,7 @@ Binary *db_match_set_compile(Process *p, Eterm matchexpr,
 	if (!is_tuple(t) || (tp = tuple_val(t))[0] != make_arityval(3)) {
 	    goto error;
 	}
+	prefix_flags[i] = false;
 	if (!(flags & DCOMP_TRACE) || (!is_list(tp[1]) && 
 					!is_nil(tp[1]))) {
 	    t = tp[1];
@@ -1264,7 +1271,9 @@ Binary *db_match_set_compile(Process *p, Eterm matchexpr,
 	    for (l2 = tp[1]; is_list(l2); l2 = CDR(list_val(l2))) {
 		++n;
 	    }
-	    if (l2 != NIL) {
+	    if (l2 == am_Underscore) {
+		prefix_flags[i] = true;
+	    } else if (l2 != NIL) {
 		goto error;
 	    }
 	    hp = HAlloc(p, n + 1);
@@ -1289,13 +1298,17 @@ Binary *db_match_set_compile(Process *p, Eterm matchexpr,
 				num_heads,
 				flags,
 				NULL,
-                                freasonp)) == NULL) {
+                                freasonp,
+                                prefix_flags)) == NULL) {
 	goto error;
     }
     compiled = 1;
     if (buff != sbuff) {
 	erts_free(ERTS_ALC_T_DB_TMP, buff);
     }
+    if (prefix_flags != sprefix) {
+	erts_free(ERTS_ALC_T_DB_TMP, prefix_flags);
+    }
     return mps;
 
 error:
@@ -1305,6 +1318,9 @@ error:
     if (buff != sbuff) {
 	erts_free(ERTS_ALC_T_DB_TMP, buff);
     }
+    if (prefix_flags != sprefix) {
+	erts_free(ERTS_ALC_T_DB_TMP, prefix_flags);
+    }
     return NULL;
 }
 
@@ -1505,6 +1521,8 @@ static Eterm db_match_set_lint(Process *p, Eterm matchexpr, Uint flags)
     Eterm *matches,*guards, *bodies;
     Eterm sbuff[15];
     Eterm *buff = sbuff;
+    bool *prefix_flags = NULL;
+    bool sprefix[5] = {0};
     int i;
     Uint freason = BADARG;
 
@@ -1526,7 +1544,11 @@ static Eterm db_match_set_lint(Process *p, Eterm matchexpr, Uint flags)
     if (num_heads > 5) {
 	buff = erts_alloc(ERTS_ALC_T_DB_TMP,
 			  sizeof(Eterm) * num_heads * 3);
-    } 
+	prefix_flags = erts_alloc(ERTS_ALC_T_DB_TMP,
+				  sizeof(bool) * num_heads);
+    } else {
+	prefix_flags = sprefix;
+    }
 
     matches = buff;
     guards = buff + num_heads;
@@ -1542,6 +1564,7 @@ static Eterm db_match_set_lint(Process *p, Eterm matchexpr, Uint flags)
 			-1, 0UL, dmcError);
 	    goto done;
 	}
+	prefix_flags[i] = false;
 	if (!(flags & DCOMP_TRACE) || (!is_list(tp[1]) && 
 					!is_nil(tp[1]))) {
 	    t = tp[1];
@@ -1550,7 +1573,9 @@ static Eterm db_match_set_lint(Process *p, Eterm matchexpr, Uint flags)
 	    for (l2 = tp[1]; is_list(l2); l2 = CDR(list_val(l2))) {
 		++n;
 	    }
-	    if (l2 != NIL) {
+	    if (l2 == am_Underscore) {
+		prefix_flags[i] = true;
+	    } else if (l2 != NIL) {
 		add_dmc_err(err_info, 
 			    "Match expression part %T is not a "
 			    "proper list.", 
@@ -1577,7 +1602,7 @@ static Eterm db_match_set_lint(Process *p, Eterm matchexpr, Uint flags)
 	++i;
     }
     mp = db_match_compile(matches, guards, bodies, num_heads,
-			  flags, err_info, &freason); 
+			  flags, err_info, &freason, prefix_flags);
     if (mp != NULL) {
 	erts_bin_free(mp);
     }
@@ -1587,6 +1612,9 @@ done:
     if (buff != sbuff) {
 	erts_free(ERTS_ALC_T_DB_TMP, buff);
     }
+    if (prefix_flags != sprefix) {
+	erts_free(ERTS_ALC_T_DB_TMP, prefix_flags);
+    }
     return ret;
 }
     
@@ -1692,7 +1720,8 @@ Binary *db_match_compile(Eterm *matchexpr,
 			 int num_progs,
 			 Uint flags, 
 			 DMCErrInfo *err_info,
-                         Uint *freasonp)
+                         Uint *freasonp,
+                         const bool *is_prefix)
 {
     DMCHeap heap;
     DMC_STACK_TYPE(Eterm) stack;
@@ -1959,7 +1988,11 @@ restart:
 		    }
 		    goto error;
 		}
-		DMC_POKE(text, clause_start, matchArray);
+		if (is_prefix && is_prefix[context.current_match]) {
+		    DMC_POKE(text, clause_start, matchArrayPrefix);
+		} else {
+		    DMC_POKE(text, clause_start, matchArray);
+		}
 	    }
 	}
 
@@ -2257,6 +2290,12 @@ restart:
 		FAIL();
 	    ep = termp;
 	    break;
+	case matchArrayPrefix: /* only when DCOMP_TRACE, prefix match */
+	    n = *pc++;
+	    if ((int) n > arity)
+		FAIL();
+	    ep = termp;
+	    break;
 	case matchArrayBind: /* When the array size is unknown. */
 	    ASSERT(termp || arity==0);
 	    n = *pc++;
@@ -6211,6 +6250,12 @@ void db_match_dis(Binary *bp)
 	    ++t;
 	    erts_printf("Array\t%beu\n", n);
 	    break;
+	case matchArrayPrefix:
+	    ++t;
+	    n = *t;
+	    ++t;
+	    erts_printf("ArrayPrefix\t%beu\n", n);
+	    break;
 	case matchArrayBind:
 	    ++t;
 	    n = *t;
diff --git a/erts/emulator/beam/erl_db_util.h b/erts/emulator/beam/erl_db_util.h
index 13c5dc58cd..0c4e23fc30 100644
--- a/erts/emulator/beam/erl_db_util.h
+++ b/erts/emulator/beam/erl_db_util.h
@@ -568,7 +568,8 @@ Binary *db_match_compile(Eterm *matchexpr, Eterm *guards,
 			 Eterm *body, int num_matches, 
 			 Uint flags, 
 			 DMCErrInfo *err_info,
-                         Uint *freasonp);
+                         Uint *freasonp,
+                         const bool *is_prefix);
 /* Returns newly allocated MatchProg binary with refc == 0*/
 
 Eterm db_match_dbterm_uncompressed(DbTableCommon* tb, Process* c_p, Binary* bprog,
diff --git a/erts/emulator/test/call_trace_SUITE.erl b/erts/emulator/test/call_trace_SUITE.erl
index 411a36d1dd..4a1c499504 100644
--- a/erts/emulator/test/call_trace_SUITE.erl
+++ b/erts/emulator/test/call_trace_SUITE.erl
@@ -27,14 +27,15 @@
 
 -export([all/0, suite/0,
          init_per_testcase/2,end_per_testcase/2,
-         process_specs/1,basic/1,flags/1,errors/1,pam/1,change_pam/1,
+         process_specs/1,basic/1,flags/1,errors/1,pam/1,pam_prefix/1,change_pam/1,
          return_trace/1,exception_trace/1,on_load/1,deep_exception/1,
          upgrade/1,
          exception_nocatch/1,bit_syntax/1]).
 
 %% Helper functions.
 
--export([bar/0,foo/0,foo/1,foo/2,expect/1,worker_foo/1,pam_foo/2,nasty/0,
+-export([bar/0,foo/0,foo/1,foo/2,expect/1,worker_foo/1,pam_foo/2,
+         prefix_foo/1,prefix_foo/2,prefix_foo/3,nasty/0,
          id/1,deep/3,deep_1/3,deep_2/2,deep_3/2,deep_4/1,deep_5/1,
          bs_sum_a/2,bs_sum_b/2]).
 
@@ -61,7 +62,7 @@ groups() ->
      {timetrap, {minutes, 2}}].
 
 all() ->
-    [process_specs, basic, flags, pam, change_pam,
+    [process_specs, basic, flags, pam, pam_prefix, change_pam,
      upgrade,
      return_trace, exception_trace, deep_exception,
      exception_nocatch, bit_syntax, errors, on_load].
@@ -565,7 +566,115 @@ pam_foo(A, B) ->
     {ok,A,B}.
 
 
-%% Test changing PAM programs for a function.
+%% Test prefix matching in PAM (match on argument prefix).
+pam_prefix(Config) when is_list(Config) ->
+    start_tracer(),
+    Self = self(),
+    trace_pid(Self, true, [call]),
+
+    %% Test Prog1: prefix match on first argument being {a, tuple}
+    Prog1 = {[{a,tuple} | '_'],[],[]},
+    trace_func({?MODULE,prefix_foo,'_'}, [Prog1]),
+
+    ?MODULE:prefix_foo(not_a_tuple),
+    ?MODULE:prefix_foo({a,tuple}),
+    ?MODULE:prefix_foo({a,tuple}, extra_arg),
+    ?MODULE:prefix_foo({a,tuple}, extra1, extra2),
+    ?MODULE:prefix_foo(something_else, extra_arg),
+
+    expect({trace,Self,call,{?MODULE,prefix_foo,[{a,tuple}]}}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[{a,tuple},extra_arg]}}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[{a,tuple},extra1,extra2]}}),
+
+    trace_func({?MODULE,prefix_foo,'_'}, false),
+
+    %% Test Prog2: prefix match with guard on first argument
+    Prog2 = {['$1' | '_'],[{'>','$1',5}],[{message,'$1'}]},
+    trace_func({?MODULE,prefix_foo,'_'}, [Prog2]),
+
+    ?MODULE:prefix_foo(3),
+    ?MODULE:prefix_foo(10),
+    ?MODULE:prefix_foo(7, second),
+    ?MODULE:prefix_foo(2, second, third),
+    ?MODULE:prefix_foo(100, second, third),
+
+    expect({trace,Self,call,{?MODULE,prefix_foo,[10]},10}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[7,second]},7}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[100,second,third]},100}),
+
+    trace_func({?MODULE,prefix_foo,'_'}, false),
+
+    %% Test empty prefix: ['_'] matches any arity (equivalent to '_')
+    Prog3 = {['$1' | '_'],[],[{message,'$1'}]},
+    trace_func({?MODULE,prefix_foo,'_'}, [Prog3]),
+
+    ?MODULE:prefix_foo(single),
+    ?MODULE:prefix_foo(first, second_arg),
+
+    expect({trace,Self,call,{?MODULE,prefix_foo,[single]},single}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[first,second_arg]},first}),
+
+    trace_func({?MODULE,prefix_foo,'_'}, false),
+
+    %% Test '$_' (whole expression) in prefix matches.
+    %% '$_' should return ALL actual arguments, not just the matched prefix.
+    Prog4 = {['$1' | '_'],[],[{message,'$_'}]},
+    trace_func({?MODULE,prefix_foo,'_'}, [Prog4]),
+
+    ?MODULE:prefix_foo(single),
+    ?MODULE:prefix_foo(first, second_arg),
+    ?MODULE:prefix_foo(first, second_arg, third_arg),
+
+    expect({trace,Self,call,{?MODULE,prefix_foo,[single]},[single]}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[first,second_arg]},
+            [first,second_arg]}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[first,second_arg,third_arg]},
+            [first,second_arg,third_arg]}),
+
+    trace_func({?MODULE,prefix_foo,'_'}, false),
+
+    %% Test '$$' (all bindings) in prefix matches.
+    %% '$$' should return only variables bound in the prefix head.
+    Prog5 = {['$1','$2' | '_'],[],[{message,'$$'}]},
+    trace_func({?MODULE,prefix_foo,'_'}, [Prog5]),
+
+    %% prefix_foo/1 should NOT match (only 1 arg, prefix needs at least 2)
+    ?MODULE:prefix_foo(only_one),
+    %% prefix_foo/2 and /3 should match
+    ?MODULE:prefix_foo(first, second_arg),
+    ?MODULE:prefix_foo(first, second_arg, third_arg),
+
+    expect({trace,Self,call,{?MODULE,prefix_foo,[first,second_arg]},
+            [first,second_arg]}),
+    expect({trace,Self,call,{?MODULE,prefix_foo,[first,second_arg,third_arg]},
+            [first,second_arg]}),
+
+    trace_func({?MODULE,prefix_foo,'_'}, false),
+
+    %% Test erlang:match_spec_test with prefix match
+    {ok,a,[],[]} = erlang:match_spec_test(
+        [a,b,c], [{['$1' | '_'],[],[{message,'$1'}]}], trace),
+    {ok,false,[],[]} = erlang:match_spec_test(
+        [3], [{['$1' | '_'],[{'>','$1',5}],[{message,'$1'}]}], trace),
+    {ok,10,[],[]} = erlang:match_spec_test(
+        [10,extra], [{['$1' | '_'],[{'>','$1',5}],[{message,'$1'}]}], trace),
+    {ok,[a,b,c],[],[]} = erlang:match_spec_test(
+        [a,b,c], [{['$1' | '_'],[],[{message,'$_'}]}], trace),
+    {ok,[a,b],[],[]} = erlang:match_spec_test(
+        [a,b], [{['$1' | '_'],[],[{message,'$_'}]}], trace),
+    {ok,[a,b],[],[]} = erlang:match_spec_test(
+        [a,b,c], [{['$1','$2' | '_'],[],[{message,'$$'}]}], trace),
+    {ok,[a],[],[]} = erlang:match_spec_test(
+        [a,b,c], [{['$1' | '_'],[],[{message,'$$'}]}], trace),
+    ok.
+
+prefix_foo(A) ->
+    {ok, A}.
+prefix_foo(A, B) ->
+    {ok, A, B}.
+prefix_foo(A, B, C) ->
+    {ok, A, B, C}.
+
 change_pam(_Config) ->
     start_tracer(),
     Self = self(),
-- 
2.51.0

openSUSE Build Service is sponsored by