Skip to content

Process aliases#2027

Open
mat-hek wants to merge 22 commits intoatomvm:mainfrom
mat-hek:mf/upstream-aliases
Open

Process aliases#2027
mat-hek wants to merge 22 commits intoatomvm:mainfrom
mat-hek:mf/upstream-aliases

Conversation

@mat-hek
Copy link
Contributor

@mat-hek mat-hek commented Dec 8, 2025

TODO:

  • support aliases in spawn_opt
  • reply_demonitor
  • C docs
  • erlang docs
  • process reference serialisation

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@mat-hek mat-hek force-pushed the mf/upstream-aliases branch 8 times, most recently from e2c4216 to f30b1f3 Compare December 9, 2025 09:18
@mat-hek
Copy link
Contributor Author

mat-hek commented Dec 9, 2025

@bettio it seems that CI sometimes fails because it cannot find erlang:alias. I added it to erlang.erl, nifs.gperf and nifs.c, am I missing something?

#define BOXED_FUN_SIZE 3
#define FLOAT_SIZE (sizeof(float_term_t) / sizeof(term) + 1)
#define REF_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1))
#define TERM_BOXED_PID_REF_SIZE (REF_SIZE + 1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the work we are doing lately I suggest renaming all PID_REF and pid_reference to PROCESS_REF and process_reference

term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_PID_REF_SIZE);
boxed_value[0] = TERM_BOXED_PID_REF_HEADER;

#if TERM_BYTES == 8
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking/very low priority comment: just for consistency I would suggest moving first the 4 branch:
#if TERM_BYTES == 4 ... #elif TERM_BYTES == 8.
Same applies to the other functions as well.

return ret;

} else if (term_is_pid_reference(t)) {
int32_t process_id = term_pid_ref_to_process_id(t);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using uint32_t for process id.

Copy link
Contributor Author

@mat-hek mat-hek Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? It's int32 here: https://github.com/atomvm/AtomVM/blob/main/src/libAtomVM/context.h#L106 and in many other places

ok = test_alias(),
ok = test_monitor_alias(),
ok = test_monitor_alias_explicit_unalias(),
ok = test_monitor_down_alias(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also test unhappy paths, such as:

  1. double/triple unalias
  2. alias a dead process
  3. unalias a dead process

In addition multiple aliases should be tested, this is legit but is should be properly tested.

RAISE_ERROR(BADARG_ATOM);
}

while (!term_is_nil(options)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use term_is_nonempty_list to avoid issues with non proper lists.

RAISE_ERROR(UNSUPPORTED_ATOM);
} else {
RAISE_ERROR(BADARG_ATOM);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we check that last list item is nil.

enum ContextMonitorAliasType
{
CONTEXT_MONITOR_ALIAS_EXPLICIT_UNALIAS,
CONTEXT_MONITOR_ALIAS_DEMONITOR,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use PascalCase. See AVMCCS-N004 rule in our coding style guide.
(There is still some old code that has to be migrated to updated style).

@mat-hek mat-hek force-pushed the mf/upstream-aliases branch 9 times, most recently from 4b23228 to 07373d0 Compare December 12, 2025 14:47
@mat-hek mat-hek requested a review from bettio December 12, 2025 16:41
@mat-hek
Copy link
Contributor Author

mat-hek commented Dec 13, 2025

@bettio added more tests and missing features. The CI is failing, but it seems unrelated to the changes

Copy link
Collaborator

@bettio bettio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks mostly good: there is only one change related to IS_NULL_PTR that is required. I suggest also rebasing on top of main, since main has a lot of fixes for flaky tests.

Context *p = globalcontext_get_process_lock(glb, local_process_id);
if (p) {
struct MonitorAlias *alias = context_find_alias(p, ref_ticks);
if (!IS_NULL_PTR(alias)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not use !IS_NULL_PTR (x) since it is a shorthand for UNLIKELY(x == NULL). So we must use it only when it is unlikely that the pointer is null (that is mostly for error handling purposes).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but there are many occurrences of !IS_NULL_PTR in the code

@bettio bettio requested a review from pguyot December 15, 2025 15:34
@mat-hek mat-hek force-pushed the mf/upstream-aliases branch 2 times, most recently from 92db4b7 to 1bdef14 Compare December 15, 2025 16:13
@pguyot
Copy link
Collaborator

pguyot commented Dec 15, 2025

esp32 tests are crashing

Copy link
Collaborator

@pguyot pguyot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main concern so far is that monitors are now extended to 2 or 3 words instead of 1 or 2. This may break some code that took monitors and passed it around as references, as the term_to_ref_ticks/term_from_ref_ticks is a common pattern. A similar breakage existed with resources being larger references if one tries to pass a resource where a 1/2 words ref is expected, but it is less likely as resources were not references so far. This may be the crash cause of esp32 tests.

port.erl

call(Port, Message, Timeout) ->
    MonitorRef = monitor(port, Port),
    Port ! {'$call', {self(), MonitorRef}, Message},
    Result =
        receive
            {'DOWN', MonitorRef, port, Port, normal} ->
                {error, noproc};
            {'DOWN', MonitorRef, port, Port, Reason} ->
                {error, Reason};
            out_of_memory ->
                out_of_memory;
            {MonitorRef, Ret} ->
                Ret
        after Timeout ->
            {error, timeout}
        end,
    demonitor(MonitorRef, [flush]),
    Result.

uart_driver.c:

static void uart_driver_do_read(Context *ctx, GenMessage gen_message)
{
    GlobalContext *glb = ctx->global;
    struct UARTData *uart_data = ctx->platform_data;
    term pid = gen_message.pid;
    term ref = gen_message.ref;
    uint64_t ref_ticks = term_to_ref_ticks(ref);

-type raise_stacktrace() ::
[{module(), atom(), arity() | [term()]} | {function(), arity() | [term()]}] | stacktrace().

-type monitor_option() :: {'alias', 'explicit_unalias' | 'demonitor' | 'reply_demonitor'}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not enforced by style guide but usually we don't quote atoms when it's not required.

}
// Prepare the message on ctx's heap which will be freed afterwards.
term ref = term_from_ref_ticks(monitored_monitor->ref_ticks, &ctx->heap);
term ref = term_make_process_reference(target->process_id, monitored_monitor->ref_ticks, &ctx->heap);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not what OTP does and there probably is a good reason.
Regular monitors are smaller than aliases.

7> Pid = spawn(fun() -> receive ok -> ok end end).
<0.94.0>
8> monitor(process, Pid).
#Ref<0.3008598808.1200095248.57512>
9> monitor(process, Pid, [{alias, explicit_unalias}]).
#Ref<0.0.11651.3008598808.1200160784.57533>
10> make_ref().
#Ref<0.3008598808.1200095248.57553>

boxed_value[0] = TERM_BOXED_PROCESS_REF_HEADER;

#if TERM_BYTES == 4
boxed_value[1] = (ref_ticks >> 4);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed for regular refs, we probably want ref_ticks >> 32

end,
Term.

is_atomvm_or_otp_version_at_least(OTPVersion) ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_otp_version/0 already exists in this module.

Also there is a poor usage (fixed in #2025 )

    OTPVersion = get_otp_version(),
    if
        OTPVersion =:= atomvm orelse OTPVersion >= 26 ->
            B == erlang:term_to_binary(A);
        true ->
            true
    end.

be sure to rather do:

    OTPVersion = get_otp_version(),
    if
        OTPVersion >= 26 ->
            B == erlang:term_to_binary(A);
        true ->
            true
    end.

as atoms sort higher than integers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there is a poor usage

I would call it 'a readable way' :P

@mat-hek mat-hek force-pushed the mf/upstream-aliases branch from ce57282 to eac7682 Compare December 19, 2025 09:45
@mat-hek
Copy link
Contributor Author

mat-hek commented Dec 19, 2025

@pguyot the reason for the STM32 tests failing was that the size of a regular reference is actually 3 terms there, which means process reference takes 4 terms, and it probably conflicts with resource reference size, which is always 4 terms. I changed the process reference size to 5, but that's not a perfect solution :P So we need to come up with another way of distinguishing references - do you have anything in mind?

I also added RefData as a more universal representation of references than ref_ticks. WYDT? Would it make sense for it to support resource references too?

@mat-hek mat-hek requested a review from pguyot December 19, 2025 13:16
@mat-hek mat-hek force-pushed the mf/upstream-aliases branch from dc30e6b to a0f7887 Compare February 6, 2026 14:19
@mat-hek mat-hek requested a review from bettio February 6, 2026 14:30
@bettio bettio added this to the v0.7.0 milestone Feb 6, 2026
TRACE_SEND(ctx, x_regs[0], x_regs[1]);
globalcontext_send_message(ctx->global, local_process_id, x_regs[1]);
} else if (term_is_process_reference(recipient_term)) {
int32_t local_process_id = term_process_ref_to_process_id(recipient_term);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also found the same issue with send() NIF.


globalcontext_send_message(glb, local_process_id, argv[1]);

} else if (term_is_process_reference(target)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a duplicate.

RefData ref_data = {
.type = RefTypeProcess,
.process = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried that, it cannot be left enabled by default, otherwise it would format some trivial code in a quite awful way :(


/**
* @brief Find a process alias
* @details Called within the process only
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The information here in details is not clear to me.

{
struct Monitor monitor;
uint64_t ref_ticks;
RefData ref_data;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use this as an example.
We are storing the entire the entire RefData structure even if we already know in advance: which monitor type is and that we will use only ref_ticks.
This is not efficient in terms of memory usage as soon as we starting having around several monitors.

Copy link
Contributor Author

@mat-hek mat-hek Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this particular case, RefData is needed, because when making a term out of it, we need to know whether to create a process ref or a regular ref. If we need a process ref, then we also need a process id, which we now have in RefData, which is convenient, but we could probably keep a flag instead and get process_id from the context. Still, we'd have to keep a flag, so the savings would be marginal, if any.

mat-hek added 19 commits March 5, 2026 12:09
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
@mat-hek mat-hek force-pushed the mf/upstream-aliases branch from 31b3f98 to f836ea9 Compare March 5, 2026 11:13
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
@mat-hek mat-hek force-pushed the mf/upstream-aliases branch from f836ea9 to 8651323 Compare March 5, 2026 11:15
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
@mat-hek mat-hek requested a review from bettio March 5, 2026 11:26
Signed-off-by: Mateusz Front <mateusz.front@swmansion.com>
@petermm
Copy link
Contributor

petermm commented Mar 6, 2026

Amp delivered this review: https://ampcode.com/threads/T-019cc41b-47c0-735d-a474-3fc46fe3caee - expanded here: https://gist.github.com/petermm/c3286d0cd622e7d5985a32dbca4b006d

All caveats apply - and I'm fully pragmatic about llm being both wrong and pedantic - so pick and choose, and let's land this sooner rather than later, even if there is an todo issue - with further hardening..

PR #2027 Review — Process Aliases & Process References

Branch: pr/2027
Commits reviewed: 22 (7384818..d992def)
Files changed: 25 (784 lines added, 115 removed)
Reviewer: AI-assisted deep review
Date: 2026-03-06


Summary

This PR adds Erlang/OTP-compatible process aliases to AtomVM. Key changes:

  • New RefData struct (ref_ticks + process_id) replaces raw uint64_t ref_ticks in monitors
  • New process reference boxed type (TERM_BOXED_REFERENCE_PROCESS_SIZE), distinguished from short and resource refs by size
  • New monitor type CONTEXT_MONITOR_ALIAS with three alias modes: ExplicitUnalias, Demonitor, ReplyDemonitor
  • New NIFs: erlang:alias/0, erlang:unalias/1, erlang:monitor/3
  • Send-to-alias support in opcode interpreter, JIT, and NIF send paths
  • Comprehensive Erlang test coverage for aliases

The design direction is sound and the test coverage is good. However, there are several issues that should be addressed before merge.


🔴 Critical Issues

1. term_compare — Mixed local/external path missing len==3 for process refs

Files: src/libAtomVM/term.c lines 746–829

The mixed local-vs-external reference comparison path (when one is local, one is external) does not handle process references (len==3). The len determination at lines 753–766 only checks for resource refs (→4) or falls through to short refs (→2):

if (term_is_external(t)) {
    len = term_get_external_reference_len(t);
} else if (term_is_resource_reference(t)) {
    len = 4;
} else {
    len = 2;  // ← process refs wrongly get len=2
}

Then at line 789, the else branch assumes len==4 and calls term_resource_refc_binary_ptr() on what could be a process reference — invalid memory access / crash / UB.

Impact: Crash or memory corruption when comparing a local process reference against an external reference of the same node/creation.

Fix: Add term_is_process_reference check (→ len=3) and handle len==3 data extraction in the mixed path, similar to the local-only path at lines 707–715.


2. Thread safety: send-to-alias walks target's monitor list without proper synchronization

Files: nifs.c:1675–1688, opcodesswitch.h:2831–2848, jit.c:780–793, context.c:1133–1154

The alias send pattern is:

Context *p = globalcontext_get_process_lock(glb, process_id);  // holds processes_table rdlock
alias = context_find_alias(p, ref_ticks);  // walks p->monitors_head
if (alias->alias_type == ReplyDemonitor)
    context_unalias(alias);                // removes/frees from sender thread!
mailbox_send(p, msg);
globalcontext_get_process_unlock(glb, p);

globalcontext_get_process_lock only holds processes_table rdlock — it does not serialize access to p->monitors_head. The owning process can concurrently mutate its monitor list via unalias/1, demonitor, or process teardown, leading to a data race / use-after-free / double-free.

This is especially concerning because context_unalias (called from the sender thread) does list_remove + free on the target's linked list.

Impact: Memory corruption under SMP when concurrent sends and monitor mutations occur.

Fix options:

  • Add a per-context mutex protecting monitors_head access
  • Route alias resolution through the mailbox signal mechanism (like monitors do)
  • At minimum, factor alias send logic into a single helper so all 3 call sites (opcode, JIT, NIF) stay consistent

🟠 High Severity Issues

3. Sending to non-alias refs silently succeeds instead of raising badarg

Files: nifs.c:1710, opcodesswitch.h:2847, jit.c:795

The fallback is:

} else if (!term_is_reference(target)) {
    RAISE_ERROR(BADARG_ATOM);
}

This means sending to make_ref() or a resource ref silently drops the message (no-op) instead of raising badarg. Only non-reference, non-pid, non-atom, non-process-ref targets raise an error.

Impact: Silent message loss. In Erlang/OTP, make_ref() ! Msg raises badarg.

Fix: Only allow term_is_process_reference(target) for alias send; all other refs should badarg.


4. monitor/3 self-monitor ignores alias options

File: nifs.c:4441–4447

When monitoring self, the alias option is completely ignored — it always returns a short ref without creating an alias:

if (UNLIKELY(local_process_id == ctx->process_id)) {
    // ...
    term ref = term_from_ref_ticks(ref_ticks, &ctx->heap);  // short ref, no alias
    return ref;
}

This is inconsistent with Erlang/OTP behavior where monitor(process, self(), [{alias, demonitor}]) creates an alias even though no actual monitor is needed.


5. monitor/3 noproc path always creates process ref, even without alias option

File: nifs.c:4453–4459

When the target doesn't exist, the code always creates a process ref:

term ref = term_make_process_reference(ctx->process_id, ref_ticks, &ctx->heap);

Even when no {alias, _} option was given, the returned ref is a process ref instead of a short ref. This inconsistency means the ref type depends on whether the target was alive.


6. unalias/1 accepts resource refs

File: nifs.c:6706

VALIDATE_VALUE(process_ref, term_is_local_reference);

term_is_local_reference matches all local reference types including resource refs. If a resource ref happens to have the same ref_ticks as an alias, it could falsely match. Should use term_is_process_reference or at least exclude resource refs.


7. Partial state installation on OOM in monitor/3

File: nifs.c:4520–4524

After installing monitor + alias state on both processes, the final heap allocation for the return ref can fail:

if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, ...) != MEMORY_GC_OK)) {
    RAISE_ERROR(OUT_OF_MEMORY_ATOM);  // monitor/alias already installed!
}

If this raises, monitors and aliases are installed but the caller has no ref to demonitor/unalias them.

Fix: Reserve return-term heap space before installing monitor/alias state.


🟡 Medium Severity Issues

8. Ordering inconsistency between local and ETF process refs

Files: term.c:707–715 vs external_term.c:548–555

Local process ref comparison packs as: [process_id, ticks_hi, ticks_lo]
ETF serialization encodes as: [creation, ticks_hi, ticks_lo, process_id]

This means term ordering could differ between local and deserialized representations. Equality works, but sorted data structures could behave inconsistently.

9. len==2 compare path has aliasing issue with local_data

File: term.c:771–788

In the mixed local/external path, both data and other_data can point to the same local_data[2] array if both terms happen to be local. The second write at line 785-786 overwrites the first. This is an existing bug (not introduced by this PR) but could manifest more easily now with more reference types.

10. Code duplication across alias send paths

The alias send logic is duplicated nearly identically in three places:

  • opcodesswitch.h:2831–2848
  • jit.c:780–793
  • nifs.c:1675–1688

Any fix to the concurrency issue or validation logic must be applied to all three. Consider factoring into a shared helper function.

11. process_console_message heap allocation size

File: nifs.c:1206

memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_PROCESS_SIZE, ...)

This was changed from 12 to TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_PROCESS_SIZE. The console port uses gen_message patterns which create a short ref (via term_from_ref_ticks), not a process ref. The allocation is over-sized (safe but wasteful on constrained devices).


🟢 Minor / Informational

12. REF_SIZE deprecation via _Pragma

File: term.h:163

#define REF_SIZE _Pragma("REF_SIZE is deprecated...") TERM_BOXED_REFERENCE_SHORT_SIZE

Creative approach to deprecation warnings. Works with GCC/Clang but may need testing on all target compilers (MSVC for Windows builds?).

13. Static assert ordering validates reference size hierarchy

File: term.h:188–190

Good defensive practice:

_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE < TERM_BOXED_REFERENCE_PROCESS_SIZE, ...);
_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE < TERM_BOXED_REFERENCE_RESOURCE_SIZE, ...);

14. Test coverage is good

The Erlang tests (test_monitor.erl, test_refs_ordering.erl, test_binary_to_term.erl) cover aliases well with OTP version guards. Tests exercise all three alias modes, multiple aliases, cross-process unalias, dead process monitoring, and spawn_opt + monitor/3 paths.

15. defaultatoms.def string lengths

X(EXPLICIT_UNALIAS_ATOM, "\x10", "explicit_unalias")
X(REPLY_DEMONITOR_ATOM, "\xF", "reply_demonitor")

Verified: explicit_unalias = 16 chars = \x10 ✓, reply_demonitor = 15 chars = \xF ✓.


Recommended Action Items (Priority Order)

  1. [BLOCKER] Fix term_compare mixed local/external path for len==3 process refs
  2. [BLOCKER] Address thread safety of alias send (monitor list traversal from non-owner thread)
  3. [HIGH] Make Ref ! Msg for non-alias refs raise badarg instead of silent no-op
  4. [HIGH] Fix monitor/3 self-monitor to handle alias options
  5. [HIGH] Fix monitor/3 noproc path ref type consistency
  6. [HIGH] Tighten unalias/1 validation to require process reference
  7. [HIGH] Reserve heap before installing monitor/alias state
  8. [MEDIUM] Factor alias send into shared helper to reduce code duplication
  9. [MEDIUM] Review/unify ordering between local and ETF process ref representations

Conclusion

The architectural approach is correct — using boxed type sizes to distinguish reference kinds is elegant and the RefData abstraction is clean. The feature implementation is largely complete with good test coverage. However, the two critical issues (term_compare crash and SMP alias send race) should be addressed before merging. The high-severity issues around validation and consistency should also be resolved to avoid subtle bugs in production.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants