RevPy
English | Русский
A reversible programming language embedded in Python.
The RevPy library lets you write Python functions that can be executed in two directions: forward and backward. You write an ordinary Python function (but following certain rules), and the @reversible decorator compiles it into a pair of forward and reverse functions. So you don't have to write the reverse function by hand — it's generated automatically.
from revpy import reversible, ReversibleFunction
add = ReversibleFunction(lambda x, a: x + a, lambda x, a: x - a)
@reversible
def twice(x):
y = add(1)[x]
z = add(1)[y]
return z
twice[0] # -> 2 = twice(0)
twice.rev[2] # -> 0 — the result of the automatically generated code
RevPy is inspired by reversible programming languages
(such as Janus),
but remains a tool inside Python, relying entirely on its syntax.
Features of RevPy:
- Expressiveness: there are reversible versions of
ifandwhile(the constructsif/fiandwhile/elihwrespectively). Thanks to this, arbitrary reversible algorithms can be implemented withRevPy. - Reliability: variable lifetimes are checked statically — every variable must be destroyed exactly once, and its liveness must be the same in all branches and iterations (similar to the ownership logic in
Rust); this lets you catch most errors before running the code. - Transparency: you can obtain the explicit
pythoncode generated automatically by the@reversibledecorator, copy this code, read it, and run it separately fromRevPy. - Flexibility: easily integrates with any libraries for
Python, includingnumpyandpytorch, as well as with any kind of reversibility (partial or full, bit-exact or up to computation error). - Speed: almost the same as ordinary
Python, all thanks to direct compilation instead of step-by-step interpretation. Slightly lower because of the embedded runtime reversibility checks for theif/fiandwhile/elihwconditions. - Minimalism: just two objects from
RevPyare more than enough:ReversibleFunction— for manually creating a reversible function from a pair of forward and reverse functions defined beforehand@reversible— for compiling a reversible program
- Dependency-free: requires only Python 3.9+, and no other dependencies.
The purpose of RevPy:
- to simplify the development of reversible algorithms, for example such as:
- compression algorithms
- reversible neural networks
- to bring the whole richness of Python infrastructure to reversible programming
Installation
pip install revpy-lang
Contents
- Key concepts
ReversibleFunction: basic reversible functions@reversible: reversible programming- API
- more examples
Key concepts
This section describes the basic principles of working with RevPy. Read this section before using the library — it introduces the terms on which everything else relies.
Significant information — the information that must not be lost during the execution of the program.
- What counts as "significant" and what doesn't — you decide.
- What counts as "preserving information" — you also decide. For example:
- hardware preservation of information — the original information can be restored bit-exactly
- mathematical preservation of information — the original information can be restored up to float computation error
- preservation of information only under certain conditions — "reversibility holds, as long as event X doesn't happen", where X is, for example, multiplication or division by 0 somewhere in the program
Significant program state — the part of the entire program state (part of the variables, fields, etc.) consisting of all the significant information.
Reversible function — a function in python for which there exists an inverse one, i.e. one that restores all the original significant state from the final significant state.
Basic reversible function — a reversible function that you have defined explicitly as a pair of forward and reverse functions. You can do this, for example, like this: ReversibleFunction(f, f_rev).
You must write the basic forward and reverse functions by hand. The responsibility for correctness (i.e. that these functions are inverse to each other in the sense you defined) lies entirely and completely with you.
RevPyhas no built-in basic reversible functions. Therefore you are obliged to provide them yourself.
Complex reversible function — a function automatically generated by the @reversible decorator from other reversible functions (both basic and complex).
@reversibleguarantees that if all the reversible functions used in it are correct, then the resulting function is also correct.
RevPy-API: the interface standard for reversible functions
output = f[input]
input = f.rev[output]
[]— the reversible function call operator (defined via.__getitem__).rev— the function inversion operator
Thus, duck typing is used: any object that has these methods can be considered a reversible function.
RevPy-function — any reversible function satisfying the RevPy-API.
Semantics:
- a call to
fdestroys theinputand creates theoutput - along the way, a call to
fmay modify other variables whose references are captured insidef - a call to
f.revrolls back all changes:- destroys the
outputand creates theinput - returns the significant state of all captured variables to their original significant state
- destroys the
inputandoutput— anypythonobjects (in particular, this may be atupleif by meaning you need to accept or return several objects)- in total: any reversible function destroys exactly one object and creates exactly one object
- a function may be partially reversible:
- either both
fandf.revwork correctly, undoing each other's effect - or both throw an exception during execution / loop forever
- i.e. it must not be the case that a call to
fsucceeded and returned something, while the reverse callf.revdid not restore the state (failed with an error, or returned the wrong thing, or did not terminate at all)
- either both
- a function may be reversible only under certain conditions:
- if these conditions on the input or intermediate values are met — reversibility holds
- otherwise — anything goes
ReversibleFunction: basic reversible functions
— builds a RevPy-function from a pair of ordinary functions (forward and reverse).
rf = ReversibleFunction(f, f_rev)
rf[x] # returns f(x)
rf.rev[x] # returns f_rev(x)
rf.rev.rev[x] # returns f(x)
rf(a, b=1)[x] # returns f(x, a, b=1)
rf(a, b=1).rev[x] # returns f_rev(x, a, b=1)
rf.rev(a, b=1)[x] # returns f_rev(x, a, b=1)
rf.rev(a, b=1).rev[x] # returns f(x, a, b=1)
- the first argument is considered the
input - the remaining arguments are considered captured external values
- accordingly, all arguments of
fandf_revexcept the first must have the same names and the same default values *argsand**kwargsare allowed in the definitions offandf_rev
The set of basic functions for the following parts of the README
from revpy import ReversibleFunction, reversibility_assert
import numpy as np
move = ReversibleFunction(
f=lambda x: x,
f_rev=lambda x: x,
)
add = ReversibleFunction(
f=lambda x, a: x + a,
f_rev=lambda x, a: x - a
)
mult = ReversibleFunction(
f=lambda x, a: x * a,
f_rev=lambda x, a: x / a
)
def init(none, value):
reversibility_assert(none is None)
return value
def destroy(x, value):
reversibility_assert(x == value)
return None
init = ReversibleFunction(init, destroy)
destroy = init.rev
def init_float(none, value, **kwargs):
reversibility_assert(none is None)
return value
def destroy_float(x, value, **kwargs):
reversibility_assert(np.isclose(x, value, **kwargs).all())
return None
init_float = ReversibleFunction(init_float, destroy_float)
destroy_float = init_float.rev
We have defined 5 basic reversible functions:
move— a function that does nothing- thus,
output = move[input]takes ownership from the input and transfers it to the output - the name is borrowed from move semantics in
C++ - we will use this function for packing/unpacking:
x, y = move[x_and_y]x_and_y = move[xy]
- thus,
add(a)— adding the constantato the inputmult(a)— multiplying the input by the constantainit(value)— initializing an object with the constant valuevaluedestroy(value)— the inverse of it, destroying an object with the assertion that the input equals the constant valuevalue- we will use it for any Python objects that make sense to compare via
==
init_float(value)— initializing a float with the constant valuevaluedestroy_float(value)— the inverse of it, destroying a float with the assertion that it equals the constant valuevalueup to computation error (this is the only difference from the ordinaryinit/destroy)- we will use it for float numbers and numpy arrays of any shapes
How will we understand reversibility from now on?
- for arbitrary Python objects — up to
== - for float numbers and numpy arrays — up to computation error
- reversibility is partial:
- either reversibility holds, or an error at runtime
- reversibility only under certain conditions:
- if there was no multiplication or division by 0 — reversibility holds
- otherwise — not necessarily
For runtime reversibility checks we used
reversibility_assert. This is exactly the function that@reversibleinserts into the automatically generated code forif/fiandwhile/elihw. This function simply throws aReversibilityErrorexception if the condition turns out to be false. Instead of it we could have used an ordinaryassert. We usereversibility_assertsolely for the purpose of emphasizing that the check concerns reversibility, and not something else.
@reversible: reversible programming
— generates a RevPy-function by analyzing the code of a python function.
Does 2 things:
- Generates the code of the reverse function, placing reverse instructions in reverse order.
- Performs reversibility checks:
- at compile time: checks the lifetimes of all variables
- at runtime: inserts
reversibility_assertchecks into the source code
You can obtain the automatically generated code via .src (see examples for if/fi and while/elihw).
Syntax
The following constructs are allowed, described below:
- reversible line of code
returnif/fiwhile/elihw- arbitrary line of code
Also allowed:
raiseassertelif
Not allowed (will cause a compilation error):
fortry/exceptwithyield
Recursion is allowed.
if and while may be nested inside each other any number of times and in any way.
In particular, the following are allowed:
- nested loops
ifinside a loop- a loop inside
if - etc.
Reversible line of code
created_variables = expression[destroyed_variables]
expression— any expression whose result is aRevPy-functioncreated_variablesanddestroyed_variables— any lists or tuples of local variable names
@reversible transforms such a line into a paired one that undoes the action of the original:
destroyed_variables = expression.rev[created_variables]
Here are all the rules checked at compile time concerning a single line of code:
created_variablesmust not overlap with the names appearing inexpressiondestroyed_variablesmust not overlap with the names appearing inexpression
You can think of it like this:
- destruction works first
- then variables are captured for
expression- if some of them were destroyed, they can no longer be captured
- at the end new variables are created
Examples:
y = f[x] # OK --> x = f.rev[y]
x = f[x] # OK --> x = f.rev[x]
x, (b, c) = f[x, (a, b)] # OK --> x, (a, b) = f.rev[x, (b, c)]
y = f(a, b=b, c=3)[x] # OK --> x = f(a, b=b, c=3)[y]
y = f[array[0]] # compilation error: array[0] is an expression, not a local variable name
point.x = f[x] # compilation error: point.x is an expression, not a local variable name
y, z = (f + g)[x] # OK --> x = (f + g).rev[y, z]
- OK if the result of
f + gis a reversible function ReversibleFunctiondoes not have a+operator, so in this examplefandgmust be some objects not related toRevPyfor which+returns aRevPy-function (i.e. an object with[]and.revoperators)- RevPy doesn't think about this and simply transforms the syntactic expressions
Empty input/output of a reversible function
It is also allowed to omit created_variables and/or destroyed_variables:
expression[destroyed_variables]
destroyed_variables = expression.rev[None] # paired line to the previous one
created_variables = expression[None]
expression.rev[created_variables] # paired line to the previous one
The absence of created_variables is equivalent to the assertion that the expression must return None:
expression[destroyed_variables] # the original line
reversibility_assert(expression[destroyed_variables] is None) # after @reversible
return
The last line of code must look like:
return <local variable name>
return x # OK
return x, y # compilation error: x, y is a tuple, not a local variable name
return array[0] # compilation error: array[0] is an expression, not a local variable name
If you need to return a pair of variables, simply pack them into a tuple (for example, using move):
res = move[x, y] # OK
return res # OK
if/fi
In reversible programming we must be able to execute a program in both directions — forward and backward.
How to execute if on the forward pass is clear: check the condition, and depending on it choose whether to go into the if branch or the else branch.
But how to execute if on the backward pass? — for this, immediately after exiting it, we must know exactly which branch we came from (and on the backward pass we will go into that branch). For this, the construct fi(C) is used, where the condition C must be true when exiting the if branch and false when exiting the else branch. On the forward pass this is checked by inserting reversibility_assert(C) at the end of the branches. On the backward pass if and fi swap roles: we choose the branch by the condition from fi, and use the condition from if to check when exiting the branch.
The original construct:
if condition:
<if_body>
else: # optional (empty <else_body> by default)
<else_body>
fi(reverse_condition) # optional (reverse_condition=condition by default)
Here is what is produced in the forward function after @reversible:
if condition:
<if_body>
reversibility_assert(reverse_condition)
else:
<else_body>
reversibility_assert(not reverse_condition)
Here is what is produced in the reverse function after @reversible:
if reverse_condition:
<reversed if_body>
reversibility_assert(condition)
else:
<reversed else_body>
reversibility_assert(not condition)
Example
(basic functions defined above)
# if condition is true, returns a * x + b
# otherwise returns x + 1
@reversible
def f(x, a, b, condition: bool):
if condition:
x = mult(a)[x]
x = add(b)[x]
else:
x = add(1)[x]
fi(condition) # optional
return x
f(a=2, b=3, condition=True)[1.0] # -> 5.0 = f(1.0, 2, 3, True)
f(a=2, b=3, condition=True).rev[5.0] # -> 1.0, the result of the automatically generated code
f(a=2, b=3, condition=False)[1.0] # -> 2.0 = f(1.0, 2, 3, False)
f(a=2, b=3, condition=False).rev[5.0] # -> 4.0, the result of the automatically generated code
Automatically generated code of the forward function:
print(f.src)
def f(x, a, b, condition: bool):
if condition:
x = mult(a)[x]
x = add(b)[x]
reversibility_assert(condition, "reversibility_assert is False: 'condition'")
else:
x = add(1)[x]
reversibility_assert(not condition, "reversibility_assert is False: 'not condition'")
return x
Automatically generated code of the reverse function:
print(f.rev.src)
def f_rev(x, a, b, condition: bool):
if condition:
x = add(b).rev[x]
x = mult(a).rev[x]
reversibility_assert(condition, "reversibility_assert is False: 'condition'")
else:
x = add(1).rev[x]
reversibility_assert(not condition, "reversibility_assert is False: 'not condition'")
return x
while/elihw
With while in reversible programming the situation is completely analogous to the situation with if. On the backward pass we must know when to exit the loop, and whether we need to enter it at all. For this, the paired construct to while is used — elihw, which on the forward pass turns into checks of conditions, and on the backward pass — into while.
The original construct:
while condition:
<body>
elihw(reverse_condition)
reverse_condition:
- must be false immediately before the first entry into the loop
- must be true every time the end of the loop is reached
Unlike fi, for which by default reverse_condition = condition, for elihw such a condition is almost never suitable. Therefore elihw is mandatory — without it you will get a compilation error.
Here is what is produced in the forward function after @reversible:
reversibility_assert(not reverse_condition)
while condition:
<body>
reversibility_assert(reverse_condition)
Here is what is produced in the reverse function after @reversible:
reversibility_assert(not condition)
while reverse_condition:
<reversed body>
reversibility_assert(condition)
Example
(basic functions defined above)
@reversible
def add_n_times(x, a, n=10):
i = init(0)[None]
while i < n:
x = add(a)[x]
i = add(1)[i]
elihw(i > 0)
destroy(n)[i]
return x
add_n_times(a=1, n=10)[1] # -> 11 = add_n_times(1, a=1, n=10)
add_n_times(a=1).rev[11] # -> 1, the result of the automatically generated code
print(add_n_times.src)
def add_n_times(x, a, n=10):
i = init(0)[None]
reversibility_assert(not i > 0, "reversibility_assert is False: 'not i > 0'")
while i < n:
x = add(a)[x]
i = add(1)[i]
reversibility_assert(i > 0, "reversibility_assert is False: 'i > 0'")
reversibility_assert(destroy(n)[i] is None, "reversibility_assert is False: 'destroy(n)[i] is None'")
return x
print(add_n_times.rev.src)
def add_n_times_rev(x, a, n=10):
i = destroy(n).rev[None]
reversibility_assert(not i < n, "reversibility_assert is False: 'not i < n'")
while i > 0:
i = add(1).rev[i]
x = add(a).rev[x]
reversibility_assert(i < n, "reversibility_assert is False: 'i < n'")
reversibility_assert(init(0).rev[i] is None, "reversibility_assert is False: 'init(0).rev[i] is None'")
return x
reversibility_assert — throws ReversibilityError if the condition is false.
Other lines
Arbitrary lines of code are also allowed. @reversible leaves such lines untouched — they are executed as is both on the forward and on the backward pass.
You bear full responsibility for ensuring that the execution of these lines of code does not change the significant state.
Here are the rules by which @reversible identifies these lines of code:
- the result must not be written to a variable (there must be no assignment)
- the last operation in the line must not be
[]
An example of such a line:
print(x)
Example
@reversible
def add_n_times(x, a, n=10):
i = init(0)[None]
while i < n:
x = add(a)[x]
i = add(1)[i]
print(x, end=' ')
elihw(i > 0)
destroy(n)[i]
return x
print(f"result={add_n_times(a=1)[0]}") # forward call
print(f"result={add_n_times(a=1).rev[10]}") # backward call
Output:
1 2 3 4 5 6 7 8 9 10 result=10
10 9 8 7 6 5 4 3 2 1 result=0
Reversibility check at compile-time
The main thing here is static lifetime checking. This is quite similar to how it works in rust. In a @reversible function the lifetime of every variable must be determined statically.
-
the first argument is considered to be passed by value (the function takes ownership of it, assuming responsibility for its destruction)
-
the remaining arguments are considered to be passed by reference (ownership remains with the calling function, and in the current function such variables cannot be created/destroyed)
-
after destruction (passing to
[]) the variable ceases to exist -
assignments of a value to existing variables are not allowed
-
in all lines of code the existence / absence of a variable must be determined statically (i.e. not depend on which
ifbranch we took, or how many times we executed thewhileloop) -
returndestroys the last local variable, returning ownership of it out of the function- as a result, at the end all local variables must be destroyed
@reversible checks all these conditions at the code analysis stage. If something is violated — you will see a RevpyCompilationError exception.
@reversible
def affine(
# Input variable whose ownership the function takes.
# Must be destroyed, otherwise — compilation error.
x,
# Captured variables, cannot be created/destroyed.
# Otherwise — compilation error.
a, b
):
# the only local variable here is x
# (since it is the first argument of the function)
y = mult(a)[x]
# here the variable x no longer exists;
# any use of it will cause a compilation error
# the only local variable here is y
z = add(b)[y]
# here the variable y no longer exists;
# any use of it will cause a compilation error
# the only local variable here is z
# z is the last remaining local variable, and that is what we return
return z
affine(a=2, b=3)[1.0] # → 5.0 (forward call)
affine(a=2, b=3).rev[5.0] # → 1.0 (backward call)
Examples of typical lifetime errors
if
Here are typical examples of errors related to if branching:
if C:
y = f[x] # in this branch the variable y is created, and x is destroyed
else:
pass # in this branch the variable y is NOT created, and x is NOT destroyed
fi(C)
if C:
y = f[x] # in this branch the variable y is created
# else is empty by default, and there y is NOT created, and x is NOT destroyed
Here are examples of correct code:
# OK
if C:
y = f[x]
else:
y = move[x]
# OK
if C:
x = f[x]
y = g[x]
z = h[y]
else:
z = move[x]
while
Here are typical examples of errors related to while loops:
while condition:
# with repeated execution of the loop, y will be created many times, which is forbidden
# with repeated execution of the loop, x will be destroyed many times, which is forbidden
y = f[x]
elihw(reverse_condition)
Here are examples of correct code:
# OK
while condition:
x = f[x]
y = g[x]
x = h[y]
elihw(reverse_condition)
API
Here is the list of all objects you can import.
# the most important decorator
from revpy import reversible
# for combining two inverse functions into one reversible one
from revpy import ReversibleFunction
# reversibility_assert will be needed if you want to copy the code of a reversible function (`.src`) and run it separately
# or you can use it in basic reversible functions instead of assert (optional)
from revpy import reversibility_assert
# exception classes: needed only if you want to catch them
from revpy import ReversibilityError, RevpyCompilationError
# stubs for syntactic constructs: needed only so that the code analyzer in your IDE doesn't complain about the nonexistent `elihw` and `fi`, i.e. only for beauty
from revpy import elihw, fi
@reversible
def reversible(function: types.FunctionType) -> ReversibleFunction:
"""
Compiles a function into reversible function.
- the first argument is considered the _input_
- other arguments are considered the _parameters_ (i.e. captured values)
- last line must be 'return \<variable name\>', and this variable is considered the _output_
:param function: a function written with the revpy rules
:return reversible_function: an instance of the class ReversibleFunction
"""
... # here main logic
return reversible_function
ReversibleFunction
class ReversibleFunction:
"""
A wrapper class over a pair of functions f and f_rev, combining them into a single reversible function.
The first argument of the functions is considered the value being destroyed, the rest -- parameters (which are not destroyed).
"""
def __init__(self, f, f_rev, args=(), kwargs=None):
self.f = f
self.f_rev = f_rev
self.args = args
self.kwargs = kwargs if kwargs is not None else {}
self.__signature__ = inspect.signature(f)
def __call__(self, *args, **kwargs):
return ReversibleFunction(self.f, self.f_rev, self.args + args, self.kwargs | kwargs)
@property
def rev(self):
return ReversibleFunction(self.f_rev, self.f, self.args, self.kwargs)
def __getitem__(self, input):
return self.f(input, *self.args, **self.kwargs)
@property
def src(self):
return self.f.src
reversibility_assert
def reversibility_assert(condition, message=''):
if not condition:
raise ReversibilityError(message)
Exception classes
Use them to catch compilation and runtime errors.
ReversibilityError
class ReversibilityError(Exception):
"""
Base class for reversibility errors at runtime.
"""
pass
RevpyCompilationError
@dataclass
class RevpyCompilationError(Exception):
"""
Base class for reversibility errors at the code analysis stage.
"""
msg: str
Stubs for syntactic constructs
fi
def fi(reverse_condition: bool):
"""
Stub function for the if construct
if condition:
...
else:
...
fi(reverse_condition)
:param reverse_condition: the condition that must be true at the moment of exiting the if branch and false at the moment of exiting the else branch. If fi is not provided, then by default reverse_condition = condition.
"""
pass
elihw
def elihw(reverse_condition: bool):
"""
Stub function for the while construct
while condition:
...
elihw(reverse_condition)
:param reverse_condition: the condition that must be false at the moment of entering the loop and true every time the end of the loop is reached
:type reverse_condition: bool
"""
pass
Metadata
Release files for revpy-lang 0.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| revpy_lang-0.0.0.tar.gz | 25.2 kB | Without provenance | Details for revpy_lang-0.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| revpy_lang-0.0.0-py3-none-any.whl | Python 3 | none | any | Details for revpy_lang-0.0.0-py3-none-any.whl |
Total release size: 43.7 kB
Release files / revpy_lang-0.0.0.tar.gz
| Download URL | revpy_lang-0.0.0.tar.gz |
|---|---|
| Size | 25.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2bc4831ad512e843223e02a2b76c32e281b13a1d39fe34cb3619f05db40568ec
|
|
BLAKE2b-256 checksum How to use checksums |
2fb28f770723ce623a546986f422d51d7ae714838e960a6fbb2b6c525fa08bdf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / revpy_lang-0.0.0-py3-none-any.whl
| Download URL | revpy_lang-0.0.0-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
58f3be31e6b38394387fd0698414a9baea7d0a8104af66252fb29595fede6fe5
|
|
BLAKE2b-256 checksum How to use checksums |
2ce9c740f9cf01f9e8609dd07037f5cb09198ab236d13d208d18ac18654647f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for revpy-lang 0.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.