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