Module hb_ao.erl¶
This module is the root of the device call logic of the AO-Core protocol in HyperBEAM.
Description¶
At the implementation level, every message is simply a collection of keys,
dictated by its Device
, that can be resolved in order to yield their
values. Each key may contain a link to another message or a raw value:
ao(BaseMessage, RequestMessage) -> {Status, Result}
Under-the-hood, AO-Core(BaseMessage, RequestMessage)
leads to a lookup of
the device
key of the base message, followed by the evaluation of
DeviceMod:PathPart(BaseMessage, RequestMessage)
, which defines the user
compute to be performed. If BaseMessage
does not specify a device,
~message@1.0
is assumed. The key to resolve is specified by the path
field of the message.
After each output, the HashPath
is updated to include the RequestMessage
that was executed upon it.
Because each message implies a device that can resolve its keys, as well
as generating a merkle tree of the computation that led to the result,
you can see the AO-Core protocol as a system for cryptographically chaining
the execution of combinators
. See docs/ao-core-protocol.md
for more
information about AO-Core.
The key(BaseMessage, RequestMessage)
pattern is repeated throughout the
HyperBEAM codebase, sometimes with BaseMessage
replaced with Msg1
, M1
or similar, and RequestMessage
replaced with Msg2
, M2
, etc.
The result of any computation can be either a new message or a raw literal value (a binary, integer, float, atom, or list of such values).
Devices can be expressed as either modules or maps. They can also be
referenced by an Arweave ID, which can be used to load a device from
the network (depending on the value of the load_remote_devices
and
trusted_device_signers
environment settings).
HyperBEAM device implementations are defined as follows:
DevMod:ExportedFunc : Key resolution functions. All are assumed to be
device keys (thus, present in every message that
uses it) unless specified by <code>DevMod:info()</code>.
Each function takes a set of parameters
of the form <code>DevMod:KeyHandler(Msg1, Msg2, Opts)</code>.
Each of these arguments can be ommitted if not
needed. Non-exported functions are not assumed
to be device keys.
DevMod:info : Optional. Returns a map of options for the device. All
options are optional and assumed to be the defaults if
not specified. This function can accept a <code>Message1</code> as
an argument, allowing it to specify its functionality
based on a specific message if appropriate.
info/exports : Overrides the export list of the Erlang module, such that
only the functions in this list are assumed to be device
keys. Defaults to all of the functions that DevMod
exports in the Erlang environment.
info/excludes : A list of keys that should not be resolved by the device,
despite being present in the Erlang module exports list.
info/handler : A function that should be used to handle _all_ keys for
messages using the device.
info/default : A function that should be used to handle all keys that
are not explicitly implemented by the device. Defaults to
the <code>dev_message</code> device, which contains general keys for
interacting with messages.
info/default_mod : A different device module that should be used to
handle all keys that are not explicitly implemented
by the device. Defaults to the <code>dev_message</code> device.
info/grouper : A function that returns the concurrency 'group' name for
an execution. Executions with the same group name will
be executed by sending a message to the associated process
and waiting for a response. This allows you to control
concurrency of execution and to allow executions to share
in-memory state as applicable. Default: A derivation of
Msg1+Msg2. This means that concurrent calls for the same
output will lead to only a single execution.
info/worker : A function that should be run as the 'server' loop of
the executor for interactions using the device.
The HyperBEAM resolver also takes a number of runtime options that change
the way that the environment operates:<code>update_hashpath</code>: Whether to add the <code>Msg2</code> to <code>HashPath</code> for the <code>Msg3</code>.
Default: true.<code>add_key</code>: Whether to add the key to the start of the arguments.
Default: <code><not set></code>.
Function Index¶
deep_set/4 | Recursively search a map, resolving keys, and set the value of the key at the given path. |
default_module/0* | The default device is the identity device, which simply returns the value associated with any key as it exists in its Erlang map. |
device_set/4* | Call the device's set function. |
device_set/5* | |
do_resolve_many/2* | |
ensure_message_loaded/2* | Ensure that a message is loaded from the cache if it is an ID, or a link, such that it is ready for execution. |
error_execution/5* | Handle an error in a device call. |
error_infinite/3* | Catch all return if we are in an infinite loop. |
error_invalid_intermediate_status/5* | |
error_invalid_message/3* | Catch all return if the message is invalid. |
find_exported_function/5 | Find the function with the highest arity that has the given name, if it exists. |
force_message/2 | |
get/2 | Shortcut for resolving a key in a message without its status if it is
ok . |
get/3 | |
get/4 | |
get_first/2 | take a sequence of base messages and paths, then return the value of the first message that can be resolved using a path. |
get_first/3 | |
info/2 | Get the info map for a device, optionally giving it a message if the device's info function is parameterized by one. |
info/3* | |
info_handler_to_fun/4* | Parse a handler key given by a device's info . |
internal_opts/1* | The execution options that are used internally by this module when calling itself. |
is_exported/3* | |
is_exported/4 | Check if a device is guarding a key via its exports list. |
keys/1 | Shortcut to get the list of keys from a message. |
keys/2 | |
keys/3 | |
load_device/2 | Load a device module from its name or a message ID. |
maybe_force_message/2* | Force the result of a device call into a message if the result is not
requested by the Opts . |
message_to_device/2 | Extract the device module from a message. |
message_to_fun/3 | Calculate the Erlang function that should be called to get a value for a given key from a device. |
normalize_key/1 | Convert a key to a binary in normalized form. |
normalize_key/2 | |
normalize_keys/1 | Ensure that a message is processable by the AO-Core resolver: No lists. |
normalize_keys/2 | |
remove/2 | Remove a key from a message, using its underlying device. |
remove/3 | |
resolve/2 | Get the value of a message's key by running its associated device function. |
resolve/3 | |
resolve_many/2 | Resolve a list of messages in sequence. |
resolve_stage/4* | |
resolve_stage/5* | |
resolve_stage/6* | |
set/3 | Shortcut for setting a key in the message using its underlying device. |
set/4 | |
subresolve/4* | Execute a sub-resolution. |
truncate_args/2 | Truncate the arguments of a function to the number of arguments it actually takes. |
verify_device_compatibility/2* | Verify that a device is compatible with the current machine. |
Function Details¶
deep_set/4¶
deep_set(Msg, Rest, Value, Opts) -> any()
Recursively search a map, resolving keys, and set the value of the key
at the given path. This function has special cases for handling set
calls
where the path is an empty list (/
). In this case, if the value is an
immediate, non-complex term, we can set it directly. Otherwise, we use the
device's set
function to set the value.
default_module/0 *¶
default_module() -> any()
The default device is the identity device, which simply returns the
value associated with any key as it exists in its Erlang map. It should also
implement the set
key, which returns a Message3
with the values changed
according to the Message2
passed to it.
device_set/4 *¶
device_set(Msg, Key, Value, Opts) -> any()
Call the device's set
function.
device_set/5 *¶
device_set(Msg, Key, Value, Mode, Opts) -> any()
do_resolve_many/2 *¶
do_resolve_many(MsgList, Opts) -> any()
ensure_message_loaded/2 *¶
ensure_message_loaded(MsgID, Opts) -> any()
Ensure that a message is loaded from the cache if it is an ID, or a link, such that it is ready for execution.
error_execution/5 *¶
error_execution(ExecGroup, Msg2, Whence, X4, Opts) -> any()
Handle an error in a device call.
error_infinite/3 *¶
error_infinite(Msg1, Msg2, Opts) -> any()
Catch all return if we are in an infinite loop.
error_invalid_intermediate_status/5 *¶
error_invalid_intermediate_status(Msg1, Msg2, Msg3, RemainingPath, Opts) -> any()
error_invalid_message/3 *¶
error_invalid_message(Msg1, Msg2, Opts) -> any()
Catch all return if the message is invalid.
find_exported_function/5¶
find_exported_function(Msg, Dev, Key, MaxArity, Opts) -> any()
Find the function with the highest arity that has the given name, if it exists.
If the device is a module, we look for a function with the given name.
If the device is a map, we look for a key in the map. First we try to find the key using its literal value. If that fails, we cast the key to an atom and try again.
force_message/2¶
force_message(X1, Opts) -> any()
get/2¶
get(Path, Msg) -> any()
Shortcut for resolving a key in a message without its status if it is
ok
. This makes it easier to write complex logic on top of messages while
maintaining a functional style.
Additionally, this function supports the {as, Device, Msg}
syntax, which
allows the key to be resolved using another device to resolve the key,
while maintaining the tracability of the HashPath
of the output message.
Returns the value of the key if it is found, otherwise returns the default
provided by the user, or not_found
if no default is provided.
get/3¶
get(Path, Msg, Opts) -> any()
get/4¶
get(Path, Msg, Default, Opts) -> any()
get_first/2¶
get_first(Paths, Opts) -> any()
take a sequence of base messages and paths, then return the value of the first message that can be resolved using a path.
get_first/3¶
get_first(Msgs, Default, Opts) -> any()
info/2¶
info(Msg, Opts) -> any()
Get the info map for a device, optionally giving it a message if the device's info function is parameterized by one.
info/3 *¶
info(DevMod, Msg, Opts) -> any()
info_handler_to_fun/4 *¶
info_handler_to_fun(Handler, Msg, Key, Opts) -> any()
Parse a handler key given by a device's info
.
internal_opts/1 *¶
internal_opts(Opts) -> any()
The execution options that are used internally by this module when calling itself.
is_exported/3 *¶
is_exported(Info, Key, Opts) -> any()
is_exported/4¶
is_exported(Msg, Dev, Key, Opts) -> any()
Check if a device is guarding a key via its exports
list. Defaults to
true if the device does not specify an exports
list. The info
function is
always exported, if it exists. Elements of the exludes
list are not
exported. Note that we check for info twice -- once when the device is
given but the info result is not, and once when the info result is given.
The reason for this is that info/3
calls other functions that may need to
check if a key is exported, so we must avoid infinite loops. We must, however,
also return a consistent result in the case that only the info result is
given, so we check for it in both cases.
keys/1¶
keys(Msg) -> any()
Shortcut to get the list of keys from a message.
keys/2¶
keys(Msg, Opts) -> any()
keys/3¶
keys(Msg, Opts, X3) -> any()
load_device/2¶
load_device(Map, Opts) -> any()
Load a device module from its name or a message ID. Returns {ok, Executable} where Executable is the device module. On error, a tuple of the form {error, Reason} is returned.
maybe_force_message/2 *¶
maybe_force_message(X1, Opts) -> any()
Force the result of a device call into a message if the result is not
requested by the Opts
. If the result is a literal, we wrap it in a message
and signal the location of the result inside. We also similarly handle ao-result
when the result is a single value and an explicit status code.
message_to_device/2¶
message_to_device(Msg, Opts) -> any()
Extract the device module from a message.
message_to_fun/3¶
message_to_fun(Msg, Key, Opts) -> any()
Calculate the Erlang function that should be called to get a value for a given key from a device.
This comes in 7 forms:
1. The message does not specify a device, so we use the default device.
2. The device has a handler
key in its Dev:info()
map, which is a
function that takes a key and returns a function to handle that key. We pass
the key as an additional argument to this function.
3. The device has a function of the name Key
, which should be called
directly.
4. The device does not implement the key, but does have a default handler
for us to call. We pass it the key as an additional argument.
5. The device does not implement the key, and has no default handler. We use
the default device to handle the key.
Error: If the device is specified, but not loadable, we raise an error.
Returns {ok | add_key, Fun} where Fun is the function to call, and add_key indicates that the key should be added to the start of the call's arguments.
normalize_key/1¶
normalize_key(Key) -> any()
Convert a key to a binary in normalized form.
normalize_key/2¶
normalize_key(Key, Opts) -> any()
normalize_keys/1¶
normalize_keys(Msg) -> any()
Ensure that a message is processable by the AO-Core resolver: No lists.
normalize_keys/2¶
normalize_keys(Msg1, Opts) -> any()
remove/2¶
remove(Msg, Key) -> any()
Remove a key from a message, using its underlying device.
remove/3¶
remove(Msg, Key, Opts) -> any()
resolve/2¶
resolve(Path, Opts) -> any()
Get the value of a message's key by running its associated device
function. Optionally, takes options that control the runtime environment.
This function returns the raw result of the device function call:
{ok | error, NewMessage}.
The resolver is composed of a series of discrete phases:
1: Normalization.
2: Cache lookup.
3: Validation check.
4: Persistent-resolver lookup.
5: Device lookup.
6: Execution.
7: Execution of the step
hook.
8: Subresolution.
9: Cryptographic linking.
10: Result caching.
11: Notify waiters.
12: Fork worker.
13: Recurse or terminate.
resolve/3¶
resolve(Msg1, Path, Opts) -> any()
resolve_many/2¶
resolve_many(ListMsg, Opts) -> any()
Resolve a list of messages in sequence. Take the output of the first
message as the input for the next message. Once the last message is resolved,
return the result.
A resolve_many
call with only a single ID will attempt to read the message
directly from the store. No execution is performed.
resolve_stage/4 *¶
resolve_stage(X1, Link, Msg2, Opts) -> any()
resolve_stage/5 *¶
resolve_stage(X1, Msg1, Msg2, ExecName, Opts) -> any()
resolve_stage/6 *¶
resolve_stage(X1, Func, Msg1, Msg2, ExecName, Opts) -> any()
set/3¶
set(RawMsg1, RawMsg2, Opts) -> any()
Shortcut for setting a key in the message using its underlying device.
Like the get/3
function, this function honors the error_strategy
option.
set
works with maps and recursive paths while maintaining the appropriate
HashPath
for each step.
set/4¶
set(Msg1, Key, Value, Opts) -> any()
subresolve/4 *¶
subresolve(RawMsg1, DevID, ReqPath, Opts) -> any()
Execute a sub-resolution.
truncate_args/2¶
truncate_args(Fun, Args) -> any()
Truncate the arguments of a function to the number of arguments it actually takes.
verify_device_compatibility/2 *¶
verify_device_compatibility(Msg, Opts) -> any()
Verify that a device is compatible with the current machine.