diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60f3aaa..0893daf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,8 @@ jobs: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: - otp-version: "26.2" + otp-version: "27.0" rebar3-version: "3" - gleam-version: "1.5.1" + gleam-version: "1.10.0" - run: gleam test - run: gleam format --check src test diff --git a/CHANGELOG.md b/CHANGELOG.md index f666155..2a4a42c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,58 @@ # Changelog +## v1.0.0-rc1 - Unreleased + +- In the `gleam/erlang/process` module: + - The `Name` type has been introduced. This type is used to give processes + names, making it easier to pass references around your application, and to + have a new process take over a role from a previous one that has crashed. + - The `new_name` function has been added for creating new names. + - The `named_subject` function has been added for creating a subject for a + given name. + - The `register` function now takes a name rather than an atom. + - The `unregister` function now takes a name rather than an atom. + - The `named` function now takes a name rather than an atom. + - The `try_call` and `try_call_forever` functions have been removed in favour + of `call` and `call_forever`. + - The `CallError` type has been removed. + - The `start` function has been replaced by the `spawn` and `spawn_unlinked` + functions. These functions use the Erlang `proc_lib` spawn functions and so + benefit from the functionality described in the + [`proc_lib` documentation](https://www.erlang.org/doc/apps/stdlib/proc_lib.html). + - The `subject_owner` function now returns a result as a named subject may not + have any process registered for that name. + - The `selecting_*` functions have been replaced by the + `select_*` functions. + - The `select*` functions have been replaced by the + `selector_receive*` functions. + - The `ProcessDown` type has been removed. + - The `Down` type has been added. + - The `selecting_monitors` function has been added. + - The `selecting_specific_monitor` function has been added. + - The `deselecting_specific_monitor` function has been added. + - The `selecting_process_down` function has been removed. + - The `deselecting_process_down` function has been removed. + - The `Abnormal` variant of the `ExitReason` type now holds a `Dynamic`. + - The argument ordering and labels of `call` have changed. +- In the `gleam/erlang/node` module: + - The `to_atom` function has been removed. + - The `send` function has been removed. +- The `gleam/erlang/reference` module has been created with: + - The `Reference` type. + - The `new` function. +- The `gleam/erlang/application` module was created with: + - The `priv_directory` function, formerly of the `gleam/erlang` module. + - The `StartType` type. +- In the `gleam/erlang/atom` module: + - The `AtomNotLoaded` type has been removed. + - The error type of `from_string` is now `Nil`. + - The `from_string` function has been renamed to `get`. + - The `create_from_string` function has been renamed to `create`. +- The `gleam/erlang` module has been removed. +- The `gleam/erlang/os` module has been removed. The [input](https://github.com/bcpeinhardt/input) + and [envoy](https://github.com/lpil/envoy) packages may be a suitable + replacement. + ## v0.34.0 - 2025-02-02 - Fixed deprecation warnings with the Gleam standard library v0.53.0 or later. diff --git a/README.md b/README.md index 7a4389f..c8988fd 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,17 @@ A library for making use of Erlang specific code! -## Features - -- Typed Erlang processes and message sending. -- Erlang binary format (de)serialisation. -- Functions for working with Erlang's charlists. -- Basic distributed Erlang support and working with nodes. -- Reading and writing of environment variables. -- Functions for working with atoms. - -## Usage - -Add this library to your Gleam project - ```shell -gleam add gleam_erlang +gleam add gleam_erlang@1 ``` - -And then use it in your code - ```gleam import gleam/io import gleam/erlang/process pub fn main() { - let fun = fn() { + process.spawn(fn() { io.println("Hello from another process running concurrently!") - } - process.start(fun, True) + }) } ``` diff --git a/gleam.toml b/gleam.toml index 1ac21a2..d80d4a1 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,6 +1,6 @@ name = "gleam_erlang" -version = "0.34.0" +version = "1.0.0-rc1" licences = ["Apache-2.0"] description = "A Gleam library for working with Erlang" @@ -9,7 +9,7 @@ links = [ { title = "Website", href = "https://gleam.run" }, { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, ] -gleam = ">= 0.32.0" +gleam = ">= 1.7.0" [dependencies] gleam_stdlib = ">= 0.53.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 40f80e1..e6fa17e 100644 --- a/manifest.toml +++ b/manifest.toml @@ -2,7 +2,7 @@ # You typically do not need to edit this file packages = [ - { name = "gleam_stdlib", version = "0.53.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "53F3E1E56F692C20FA3E0A23650AC46592464E40D8EF3EC7F364FB328E73CDF5" }, + { name = "gleam_stdlib", version = "0.58.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "091F2D2C4A3A4E2047986C47E2C2C9D728A4E068ABB31FDA17B0D347E6248467" }, { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, ] diff --git a/src/gleam/erlang.gleam b/src/gleam/erlang.gleam deleted file mode 100644 index c98ba04..0000000 --- a/src/gleam/erlang.gleam +++ /dev/null @@ -1,224 +0,0 @@ -import gleam/dynamic.{type Dynamic} -import gleam/dynamic/decode.{type DecodeError} -import gleam/erlang/atom.{type Atom} -import gleam/erlang/charlist.{type Charlist} -import gleam/list - -@external(erlang, "io_lib", "format") -fn erl_format(a: String, b: List(a)) -> Charlist - -/// Return a string representation of any term -/// -/// # Example -/// -/// ```gleam -/// erlang.format(input) -/// // -> {ok,<<"Gleam\n">>}% -/// ``` -pub fn format(term: any) -> String { - charlist.to_string(erl_format("~p", [term])) -} - -/// Returns a `BitArray` representing given value as an [Erlang external term][1]. -/// -/// -/// -/// [1]: https://www.erlang.org/doc/apps/erts/erl_ext_dist -@external(erlang, "erlang", "term_to_binary") -pub fn term_to_binary(a: a) -> BitArray - -type Safe { - Safe -} - -@external(erlang, "erlang", "binary_to_term") -fn erl_binary_to_term(a: BitArray, b: List(Safe)) -> Dynamic - -/// Decodes a value from a `BitArray` representing an [Erlang external term][1]. -/// -/// -/// -/// [1]: https://www.erlang.org/doc/apps/erts/erl_ext_dist -pub fn binary_to_term(binary: BitArray) -> Result(Dynamic, Nil) { - case rescue(fn() { erl_binary_to_term(binary, [Safe]) }) { - Ok(term) -> Ok(term) - Error(_) -> Error(Nil) - } -} - -/// Decodes a value from a trusted `BitArray` representing an -/// [Erlang external term][1]. -/// -/// *Warning*: Do not use this function with untrusted input, this can lead to -/// Denial-of-Service. More information in the [Erlang documentation][2]. -/// -/// [1]: https://www.erlang.org/doc/apps/erts/erl_ext_dist -/// [2]: https://www.erlang.org/doc/apps/erts/erlang.html#binary_to_term/1 -pub fn unsafe_binary_to_term(binary: BitArray) -> Result(Dynamic, Nil) { - case rescue(fn() { erl_binary_to_term(binary, []) }) { - Ok(term) -> Ok(term) - Error(_) -> Error(Nil) - } -} - -/// Error value returned by `get_line` function -/// -pub type GetLineError { - Eof - NoData -} - -/// Reads a line from standard input with the given prompt. -/// -/// # Example -/// -/// ```gleam -/// get_line("Language: ") -/// // > Language: <- Gleam -/// // -> Ok("Gleam\n") -/// ``` -@external(erlang, "gleam_erlang_ffi", "get_line") -pub fn get_line(prompt prompt: String) -> Result(String, GetLineError) - -pub type TimeUnit { - Second - Millisecond - Microsecond - Nanosecond -} - -/// Returns the current OS system time. -/// -/// -@external(erlang, "os", "system_time") -pub fn system_time(a: TimeUnit) -> Int - -/// Returns the current OS system time as a tuple of Ints -/// -/// -@external(erlang, "os", "timestamp") -pub fn erlang_timestamp() -> #(Int, Int, Int) - -/// Gleam doesn't offer any way to raise exceptions, but they may still occur -/// due to bugs when working with unsafe code, such as when calling Erlang -/// function. -/// -/// This function will catch any error thrown and convert it into a result -/// rather than crashing the process. -/// -@external(erlang, "gleam_erlang_ffi", "rescue") -pub fn rescue(a: fn() -> a) -> Result(a, Crash) - -pub type Crash { - Exited(Dynamic) - Thrown(Dynamic) - Errored(Dynamic) -} - -@external(erlang, "init", "get_plain_arguments") -fn get_start_arguments() -> List(Charlist) - -/// Get the arguments given to the program when it was started. -/// -/// This is sometimes called `argv` in other languages. -@deprecated("Please use the argv package instead") -pub fn start_arguments() -> List(String) { - get_start_arguments() - |> list.map(charlist.to_string) -} - -/// Starts an OTP application's process tree in the background, as well as -/// the trees of any applications that the given application depends upon. An -/// OTP application typically maps onto a Gleam or Hex package. -/// -/// Returns a list of the applications that were started. Calling this function -/// for application that have already been started is a no-op so you do not need -/// to check the application state beforehand. -/// -/// In Gleam we prefer to not use these implicit background process trees, but -/// you will likely still need to start the trees of OTP applications written in -/// other BEAM languages such as Erlang or Elixir, including those included by -/// default with Erlang/OTP. -/// -/// For more information see the OTP documentation. -/// - -/// - -/// -@external(erlang, "gleam_erlang_ffi", "ensure_all_started") -pub fn ensure_all_started( - application application: Atom, -) -> Result(List(Atom), EnsureAllStartedError) - -pub type EnsureAllStartedError { - UnknownApplication(name: Atom) - ApplicationFailedToStart(name: Atom, reason: Dynamic) -} - -/// A unique reference value. -/// -/// It holds no particular meaning or value, but unique values are often useful -/// in programs are used heavily within both Gleam and Erlang's OTP frameworks. -/// -/// More can be read about references in the [Erlang documentation][1]. -/// -/// [1]: https://www.erlang.org/doc/efficiency_guide/advanced.html#unique_references -/// -pub type Reference - -/// Create a new unique reference. -/// -@external(erlang, "erlang", "make_ref") -pub fn make_reference() -> Reference - -/// Checks to see whether a `Dynamic` value is a Reference, and return the Reference if -/// it is. -/// -/// ## Examples -/// -/// ```gleam -/// import gleam/dynamic -/// -/// reference_from_dynamic(dynamic.from(make_reference())) -/// // -> Ok(Reference) -/// ``` -/// -/// ```gleam -/// import gleam/dynamic -/// -/// reference_from_dynamic(dynamic.from(123)) -/// // -> Error([DecodeError(expected: "Reference", found: "Int", path: [])]) -/// ``` -@external(erlang, "gleam_erlang_ffi", "reference_from_dynamic") -pub fn reference_from_dynamic( - from from: Dynamic, -) -> Result(Reference, List(DecodeError)) - -/// Returns the path of a package's `priv` directory, where extra non-Gleam -/// or Erlang files are typically kept. -/// -/// Returns an error if no package was found with the given name. -/// -/// # Example -/// -/// ```gleam -/// erlang.priv_directory("my_app") -/// // -> Ok("/some/location/my_app/priv") -/// ``` -/// -@external(erlang, "gleam_erlang_ffi", "priv_directory") -pub fn priv_directory(name: String) -> Result(String, Nil) - -/// Portable hash function that gives the same hash for the -/// same Gleam/Erlang term regardless of machine architecture and -/// ERTS version. -/// -/// The function returns a hash value for Term within the range 0..limit-1. -/// The maximum value for limit is 2^32. -/// -/// -@external(erlang, "erlang", "phash2") -pub fn bounded_phash2(term: anything, limit limit: Int) -> Int - -/// Equivalent to `bounded_phash2`, with a upper limit of 2^27-1 -@external(erlang, "erlang", "phash2") -pub fn phash2(term: anything) -> Int diff --git a/src/gleam/erlang/application.gleam b/src/gleam/erlang/application.gleam new file mode 100644 index 0000000..e5c6a40 --- /dev/null +++ b/src/gleam/erlang/application.gleam @@ -0,0 +1,37 @@ +//// An Erlang application is a collection of code that can be loaded into the +//// Erlang virtual machine and even started and stopped if they define a +//// start module and supervision tree. Each Gleam package is an Erlang +//// application. + +import gleam/erlang/node.{type Node} + +/// The Erlang/OTP application `start` callback takes a start-type as an +/// argument, indicating the context in which the application is being started. +pub type StartType { + /// A normal application start. + Normal + /// The application is distributed and started at the current node because of + /// a takeover from Node, either because Erlang's `application:takeover/2` + /// function has been called, or because the current node has higher priority + /// than the previous node. + Takeover(previous: Node) + /// The application is distributed and started at the current node because of + /// a failover from the previous node. + Failover(previous: Node) +} + +/// Returns the path of an application's `priv` directory, where extra non-Gleam +/// or Erlang files are typically kept. Each Gleam package is an Erlang +/// application. +/// +/// Returns an error if no application was found with the given name. +/// +/// # Example +/// +/// ```gleam +/// application.priv_directory("my_app") +/// // -> Ok("/some/location/my_app/priv") +/// ``` +/// +@external(erlang, "gleam_erlang_ffi", "priv_directory") +pub fn priv_directory(name: String) -> Result(String, Nil) diff --git a/src/gleam/erlang/atom.gleam b/src/gleam/erlang/atom.gleam index 33931ac..0f47bd7 100644 --- a/src/gleam/erlang/atom.gleam +++ b/src/gleam/erlang/atom.gleam @@ -1,6 +1,3 @@ -import gleam/dynamic.{type Dynamic} -import gleam/dynamic/decode.{type DecodeError} - /// Atom is a special string-like data-type that is most commonly used for /// interfacing with code written in other BEAM languages such as Erlang and /// Elixir. It is preferable to define your own custom types to use instead of @@ -18,29 +15,13 @@ import gleam/dynamic/decode.{type DecodeError} /// pub type Atom -/// An error returned when no atom is found in the virtual machine's atom table -/// for a given string when calling the [`from_string`](#from_string) function. -pub type FromStringError { - AtomNotLoaded -} - -/// Finds an existing Atom for the given String. +/// Finds an existing atom for the given string. /// -/// If no atom is found in the virtual machine's atom table for the String then +/// If no atom is found in the virtual machine's atom table for the string then /// an error is returned. /// -/// ## Examples -/// ```gleam -/// from_string("ok") -/// // -> Ok(create_from_string("ok")) -/// ``` -/// ```gleam -/// from_string("some_new_atom") -/// // -> Error(AtomNotLoaded) -/// ``` -/// @external(erlang, "gleam_erlang_ffi", "atom_from_string") -pub fn from_string(a: String) -> Result(Atom, FromStringError) +pub fn get(a: String) -> Result(Atom, Nil) /// Creates an atom from a string, inserting a new value into the virtual /// machine's atom table if an atom does not already exist for the given @@ -52,34 +33,17 @@ pub fn from_string(a: String) -> Result(Atom, FromStringError) /// virtual machine to crash! /// @external(erlang, "erlang", "binary_to_atom") -pub fn create_from_string(a: String) -> Atom +pub fn create(a: String) -> Atom /// Returns a `String` corresponding to the text representation of the given /// `Atom`. /// /// ## Examples /// ```gleam -/// let ok_atom = create_from_string("ok") +/// let ok_atom = create("ok") /// to_string(ok_atom) /// // -> "ok" /// ``` /// @external(erlang, "erlang", "atom_to_binary") pub fn to_string(a: Atom) -> String - -/// Checks to see whether a `Dynamic` value is an atom, and return the atom if -/// it is. -/// -/// ## Examples -/// ```gleam -/// import gleam/dynamic -/// from_dynamic(dynamic.from(create_from_string("hello"))) -/// // -> Ok(create_from_string("hello")) -/// ``` -/// ```gleam -/// from_dynamic(dynamic.from(123)) -/// // -> Error([DecodeError(expected: "Atom", found: "Int", path: [])]) -/// ``` -/// -@external(erlang, "gleam_erlang_ffi", "atom_from_dynamic") -pub fn from_dynamic(from from: Dynamic) -> Result(Atom, List(DecodeError)) diff --git a/src/gleam/erlang/charlist.gleam b/src/gleam/erlang/charlist.gleam index e5b6d65..f7e94da 100644 --- a/src/gleam/erlang/charlist.gleam +++ b/src/gleam/erlang/charlist.gleam @@ -5,21 +5,20 @@ //// interfacing with Erlang, in particular when using older libraries that do //// not accept binaries as arguments. -/// A list of characters represented as ints. Commonly used by older Erlang -/// modules. +/// A list of characters represented as ints. Commonly used with Erlang +/// functions that do not accept binary strings such as Gleam's core string +/// type. +/// pub type Charlist -/// Transform a charlist to a string +/// Convert a charlist to a string using Erlang's +/// `unicode:characters_to_binary`. +/// @external(erlang, "unicode", "characters_to_binary") pub fn to_string(a: Charlist) -> String -// Calls `unicode:characters_to_binary(Data, unicode, unicode)` -// Note: `unicode is an alias for utf8` -// See - -/// Transform a string to a charlist +/// Convert a string to a charlist using Erlang's +/// `unicode:characters_to_list`. +/// @external(erlang, "unicode", "characters_to_list") pub fn from_string(a: String) -> Charlist -// Calls `unicode:characters_to_list(Data, unicode)` -// Note: `unicode is an alias for utf8` -// See diff --git a/src/gleam/erlang/node.gleam b/src/gleam/erlang/node.gleam index 339415c..b045555 100644 --- a/src/gleam/erlang/node.gleam +++ b/src/gleam/erlang/node.gleam @@ -1,9 +1,17 @@ +//// Multiple Erlang VM instances can form a cluster to make a distributed +//// Erlang system, talking directly to each other using messages rather than +//// other communication protocols like HTTP. In a distributed Erlang system +//// each virtual machine is called a _node_. This module provides Node related +//// types and functions to be used as a foundation by other packages providing +//// more specialised functionality. +//// +//// For more information on distributed Erlang systems see the Erlang +//// documentation: . + import gleam/erlang/atom.{type Atom} pub type Node -type DoNotLeak - /// Return the current node. /// @external(erlang, "erlang", "node") @@ -42,21 +50,3 @@ pub type ConnectError { /// @external(erlang, "gleam_erlang_ffi", "connect_node") pub fn connect(node: Atom) -> Result(Node, ConnectError) - -// TODO: test -/// Send a message to a named process on a given node. -/// -/// These messages are untyped, like regular Erlang messages. -/// -pub fn send(node: Node, name: Atom, message: message) -> Nil { - raw_send(#(name, node), message) - Nil -} - -@external(erlang, "erlang", "send") -fn raw_send(receiver: #(Atom, Node), message: message) -> DoNotLeak - -/// Convert a node to the atom of its name. -/// -@external(erlang, "gleam_erlang_ffi", "identity") -pub fn to_atom(node: Node) -> Atom diff --git a/src/gleam/erlang/os.gleam b/src/gleam/erlang/os.gleam deleted file mode 100644 index feb61b0..0000000 --- a/src/gleam/erlang/os.gleam +++ /dev/null @@ -1,36 +0,0 @@ -/// Represents operating system kernels -pub type OsFamily { - // The family which includes modern versions of the Windows operating system. - WindowsNt - // The family of operating systems based on the open source Linux kernel. - Linux - // The family of Apple operating systems such as macOS and iOS. - Darwin - // The family of operating systems based on the FreeBSD kernel. - FreeBsd - // An operating system kernel other than Linux, Darwin, FreeBSD, or NT. - Other(String) -} - -/// Returns the kernel of the host operating system. -/// -/// Unknown kernels are reported as `Other(String)`; e.g. `Other("sunos")`. -/// -/// ## Examples -/// ```gleam -/// family() -/// // -> Linux -/// ``` -/// -/// ```gleam -/// family() -/// // -> Darwin -/// ``` -/// -/// ```gleam -/// family() -/// // -> Other("sunos") -/// ``` -/// -@external(erlang, "gleam_erlang_ffi", "os_family") -pub fn family() -> OsFamily diff --git a/src/gleam/erlang/port.gleam b/src/gleam/erlang/port.gleam index 4264e55..4e1b4d8 100644 --- a/src/gleam/erlang/port.gleam +++ b/src/gleam/erlang/port.gleam @@ -1,6 +1,3 @@ -import gleam/dynamic.{type Dynamic} -import gleam/dynamic/decode.{type DecodeError} - /// Ports are how code running on the Erlang virtual machine interacts with /// the outside world. Bytes of data can be sent to and read from ports, /// providing a form of message passing to an external program or resource. @@ -10,18 +7,3 @@ import gleam/dynamic/decode.{type DecodeError} /// [1]: https://erlang.org/doc/reference_manual/ports.html /// pub type Port - -/// Checks to see whether a `Dynamic` value is a port, and return the port if -/// it is. -/// -/// ## Examples -/// -/// > import gleam/dynamic -/// > port_from_dynamic(dynamic.from(process.self())) -/// Ok(process.self()) -/// -/// > port_from_dynamic(dynamic.from(123)) -/// Error([DecodeError(expected: "Port", found: "Int", path: [])]) -/// -@external(erlang, "gleam_erlang_ffi", "port_from_dynamic") -pub fn port_from_dynamic(from from: Dynamic) -> Result(Port, List(DecodeError)) diff --git a/src/gleam/erlang/process.gleam b/src/gleam/erlang/process.gleam index 88d5e61..4ef59f9 100644 --- a/src/gleam/erlang/process.gleam +++ b/src/gleam/erlang/process.gleam @@ -1,7 +1,8 @@ import gleam/dynamic.{type Dynamic} -import gleam/dynamic/decode.{type DecodeError} -import gleam/erlang.{type Reference} +import gleam/dynamic/decode import gleam/erlang/atom.{type Atom} +import gleam/erlang/port.{type Port} +import gleam/erlang/reference.{type Reference} import gleam/string /// A `Pid` (or Process identifier) is a reference to an Erlang process. Each @@ -11,33 +12,45 @@ import gleam/string pub type Pid /// Get the `Pid` for the current process. +/// @external(erlang, "erlang", "self") pub fn self() -> Pid /// Create a new Erlang process that runs concurrently to the creator. In other /// languages this might be called a fibre, a green thread, or a coroutine. /// -/// If `linked` is `True` then the created process is linked to the creator -/// process. When a process terminates an exit signal is sent to all other -/// processes that are linked to it, causing the process to either terminate or -/// have to handle the signal. +/// The child process is linked to the creator process. When a process +/// terminates an exit signal is sent to all other processes that are linked to +/// it, causing the process to either terminate or have to handle the signal. +/// If you want an unlinked process use the `spawn_unlinked` function. /// /// More can be read about processes and links in the [Erlang documentation][1]. /// /// [1]: https://www.erlang.org/doc/reference_manual/processes.html /// -pub fn start(running implementation: fn() -> anything, linked link: Bool) -> Pid { - case link { - True -> spawn_link(implementation) - False -> spawn(implementation) - } -} - -@external(erlang, "erlang", "spawn") -fn spawn(a: fn() -> anything) -> Pid +/// This function starts processes via the Erlang `proc_lib` module, and as +/// such they benefit from the functionality described in the +/// [`proc_lib` documentation](https://www.erlang.org/doc/apps/stdlib/proc_lib.html). +/// +@external(erlang, "proc_lib", "spawn_link") +pub fn spawn(running: fn() -> anything) -> Pid -@external(erlang, "erlang", "spawn_link") -fn spawn_link(a: fn() -> anything) -> Pid +/// Create a new Erlang process that runs concurrently to the creator. In other +/// languages this might be called a fibre, a green thread, or a coroutine. +/// +/// Typically you want to create a linked process using the `spawn` function, +/// but creating an unlinked process may be occasionally useful. +/// +/// More can be read about processes and links in the [Erlang documentation][1]. +/// +/// [1]: https://www.erlang.org/doc/reference_manual/processes.html +/// +/// This function starts processes via the Erlang `proc_lib` module, and as +/// such they benefit from the functionality described in the +/// [`proc_lib` documentation](https://www.erlang.org/doc/apps/stdlib/proc_lib.html). +/// +@external(erlang, "proc_lib", "spawn") +pub fn spawn_unlinked(a: fn() -> anything) -> Pid /// A `Subject` is a value that processes can use to send and receive messages /// to and from each other in a well typed way. @@ -64,19 +77,79 @@ fn spawn_link(a: fn() -> anything) -> Pid /// pub opaque type Subject(message) { Subject(owner: Pid, tag: Reference) + NamedSubject(name: Name(message)) +} + +/// A name is an identity that a process can adopt, after which they will receive +/// messages sent to that name. This has two main advantages: +/// +/// - Structuring OTP programs becomes easier as a name can be passed down the +/// program from the top level, while without names subjects and pids would +/// need to be passed up from the started process and then back down to the +/// code that works with that process. +/// - A new process can adopt the name of one that previously failed, allowing +/// it to transparently take-over and handle messages that are sent to that +/// name. +/// +/// Names are globally unique as each process can have at most 1 name, and each +/// name can be registered by at most 1 process. Create all the names your +/// program needs at the start of your program and pass them down. Names are +/// Erlang atoms internally, so never create them dynamically. Generating too +/// many atoms will result in the atom table getting filled and causing the entire +/// virtual machine to crash. +/// +/// The most commonly used name functions are `new_name`, `register`, and +/// `named_subject`. +/// +pub type Name(message) + +/// Generate a new name that a process can register itself with using the +/// `register` function, and other processes can send messages to using +/// `named_subject`. +/// +/// The string argument is a prefix for the Erlang name. A unique suffix is +/// added to the prefix to make the name, removing the possibility of name +/// collisions. +/// +/// ## Safe use +/// +/// Use this function to create all the names your program needs when it +/// starts. **Never call this function dynamically** such as within a loop or +/// within a process within a supervision tree. +/// +/// Each time this function is called a new atom will be generated. Generating +/// too many atoms will result in the atom table getting filled and causing the +/// entire virtual machine to crash. +/// +@external(erlang, "gleam_erlang_ffi", "new_name") +pub fn new_name(prefix prefix: String) -> Name(message) + +/// Create a subject for a name, which can be used to send and receive messages. +/// +/// All subjects created for the same name behave identically and can be used +/// interchangably. +/// +pub fn named_subject(name: Name(message)) -> Subject(message) { + NamedSubject(name) } /// Create a new `Subject` owned by the current process. /// pub fn new_subject() -> Subject(message) { - Subject(owner: self(), tag: erlang.make_reference()) + Subject(owner: self(), tag: reference.new()) } -/// Get the owner process for a `Subject`. This is the process that created the -/// `Subject` and will receive messages sent with it. +/// Get the owner process for a subject, which is the process that will +/// receive any messages sent using the subject. +/// +/// If the subject was created from a name and no process is currently +/// registered with that name then this function will return an error. /// -pub fn subject_owner(subject: Subject(message)) -> Pid { - subject.owner +pub fn subject_owner(subject: Subject(message)) -> Result(Pid, Nil) { + case subject { + NamedSubject(name) -> named(name) + Subject(pid, _) -> Ok(pid) + } } type DoNotLeak @@ -90,6 +163,16 @@ fn raw_send(a: Pid, b: message) -> DoNotLeak /// This function does not wait for the `Subject` owner process to call the /// `receive` function, instead it returns once the message has been placed in /// the process' mailbox. +/// +/// # Named Subjects +/// +/// If this function is called on a named subject for which a process has not been +/// registered, it will simply drop the message as there's no mailbox to send it to. +/// +/// # Panics +/// +/// This function will panic when sending to a named subject if no process is +/// currently registed under that name. /// /// # Ordering /// @@ -109,7 +192,15 @@ fn raw_send(a: Pid, b: message) -> DoNotLeak /// ``` /// pub fn send(subject: Subject(message), message: message) -> Nil { - raw_send(subject.owner, #(subject.tag, message)) + case subject { + Subject(pid, tag) -> { + raw_send(pid, #(tag, message)) + } + NamedSubject(name) -> { + let assert Ok(pid) = named(name) as "Sending to unregistered name" + raw_send(pid, #(name, message)) + } + } Nil } @@ -143,19 +234,19 @@ pub fn receive_forever(from subject: Subject(message)) -> message /// A type that enables a process to wait for messages from multiple `Subject`s /// at the same time, returning whichever message arrives first. /// -/// Used with the `new_selector`, `selecting`, and `select` functions. +/// Used with the `new_selector`, `selector_receive`, and `select*` functions. /// /// # Examples /// /// ```gleam /// let int_subject = new_subject() -/// let float_subject = new_subject() +/// let string_subject = new_subject() /// send(int_subject, 1) /// /// let selector = /// new_selector() -/// |> selecting(int_subject, int.to_string) -/// |> selecting(float_subject, float.to_string) +/// |> select(string_subject) +/// |> select_map(int_subject, int.to_string) /// /// select(selector, 10) /// // -> Ok("1") @@ -170,8 +261,8 @@ pub type Selector(payload) pub fn new_selector() -> Selector(payload) /// Receive a message that has been sent to current process using any of the -/// `Subject`s that have been added to the `Selector` with the `selecting` -/// function. +/// `Subject`s that have been added to the `Selector` with the `select*` +/// functions. /// /// If there is not an existing message for the `Selector` in the process' /// mailbox or one does not arrive `within` the permitted timeout then the @@ -182,12 +273,12 @@ pub fn new_selector() -> Selector(payload) /// with it then it will not receive a message. /// /// To wait forever for the next message rather than for a limited amount of -/// time see the `select_forever` function. +/// time see the `selector_receive_forever` function. /// /// The `within` parameter specifies the timeout duration in milliseconds. /// @external(erlang, "gleam_erlang_ffi", "select") -pub fn select( +pub fn selector_receive( from from: Selector(payload), within within: Int, ) -> Result(payload, Nil) @@ -196,7 +287,7 @@ pub fn select( /// arrive rather than timing out after a specified amount of time. /// @external(erlang, "gleam_erlang_ffi", "select") -pub fn select_forever(from from: Selector(payload)) -> payload +pub fn selector_receive_forever(from from: Selector(payload)) -> payload /// Add a transformation function to a selector. When a message is received /// using this selector the transformation function is applied to the message. @@ -223,43 +314,50 @@ pub type ExitMessage { pub type ExitReason { Normal Killed - Abnormal(reason: String) + Abnormal(reason: Dynamic) } /// Add a handler for trapped exit messages. In order for these messages to be /// sent to the process when a linked process exits the process must call the /// `trap_exit` beforehand. /// -pub fn selecting_trapped_exits( +pub fn select_trapped_exits( selector: Selector(a), handler: fn(ExitMessage) -> a, ) -> Selector(a) { - let tag = atom.create_from_string("EXIT") + let tag = atom.create("EXIT") let handler = fn(message: #(Atom, Pid, Dynamic)) -> a { - let reason = message.2 - let normal = dynamic.from(Normal) - let killed = dynamic.from(Killed) - let reason = case decode.run(reason, decode.string) { - _ if reason == normal -> Normal - _ if reason == killed -> Killed - Ok(reason) -> Abnormal(reason) - Error(_) -> Abnormal(string.inspect(reason)) - } - handler(ExitMessage(message.1, reason)) + handler(ExitMessage(message.1, cast_exit_reason(message.2))) } insert_selector_handler(selector, #(tag, 3), handler) } -// TODO: implement in Gleam /// Discard all messages in the current process' mailbox. /// /// Warning: This function may cause other processes to crash if they sent a /// message to the current process and are waiting for a response, so use with /// caution. /// +/// This function may be useful in tests. +/// @external(erlang, "gleam_erlang_ffi", "flush_messages") pub fn flush_messages() -> Nil +/// Add a new `Subject` to the `Selector` so that its messages can be selected +/// from the receiver process inbox. +/// +/// See `select_map` to add subjects of a different message type. +// + +/// See `deselect` to remove a subject from a selector. +/// +pub fn select( + selector: Selector(payload), + for subject: Subject(payload), +) -> Selector(payload) { + select_map(selector, subject, fn(x) { x }) +} + /// Add a new `Subject` to the `Selector` so that its messages can be selected /// from the receiver process inbox. /// @@ -269,180 +367,51 @@ pub fn flush_messages() -> Nil /// message types. If you do not wish to transform the incoming messages in any /// way then the `identity` function can be given. /// -/// See `deselecting` to remove a subject from a selector. +/// See `deselect` to remove a subject from a selector. /// -pub fn selecting( +pub fn select_map( selector: Selector(payload), for subject: Subject(message), mapping transform: fn(message) -> payload, ) -> Selector(payload) { let handler = fn(message: #(Reference, message)) { transform(message.1) } - insert_selector_handler(selector, #(subject.tag, 2), handler) + case subject { + NamedSubject(name) -> insert_selector_handler(selector, #(name, 2), handler) + Subject(_, tag:) -> insert_selector_handler(selector, #(tag, 2), handler) + } } /// Remove a new `Subject` from the `Selector` so that its messages will not be /// selected from the receiver process inbox. /// -pub fn deselecting( +pub fn deselect( selector: Selector(payload), for subject: Subject(message), ) -> Selector(payload) { - remove_selector_handler(selector, #(subject.tag, 2)) -} - -/// Add a handler to a selector for 2 element tuple messages with a given tag -/// element in the first position. -/// -/// Typically you want to use the `selecting` function with a `Subject` instead, -/// but this function may be useful if you need to receive messages sent from -/// other BEAM languages that do not use the `Subject` type. -/// -pub fn selecting_record2( - selector: Selector(payload), - tag: tag, - mapping transform: fn(Dynamic) -> payload, -) -> Selector(payload) { - let handler = fn(message: #(tag, Dynamic)) { transform(message.1) } - insert_selector_handler(selector, #(tag, 2), handler) -} - -/// Add a handler to a selector for 3 element tuple messages with a given tag -/// element in the first position. -/// -/// Typically you want to use the `selecting` function with a `Subject` instead, -/// but this function may be useful if you need to receive messages sent from -/// other BEAM languages that do not use the `Subject` type. -/// -pub fn selecting_record3( - selector: Selector(payload), - tag: tag, - mapping transform: fn(Dynamic, Dynamic) -> payload, -) -> Selector(payload) { - let handler = fn(message: #(tag, Dynamic, Dynamic)) { - transform(message.1, message.2) - } - insert_selector_handler(selector, #(tag, 3), handler) -} - -/// Add a handler to a selector for 4 element tuple messages with a given tag -/// element in the first position. -/// -/// Typically you want to use the `selecting` function with a `Subject` instead, -/// but this function may be useful if you need to receive messages sent from -/// other BEAM languages that do not use the `Subject` type. -/// -pub fn selecting_record4( - selector: Selector(payload), - tag: tag, - mapping transform: fn(Dynamic, Dynamic, Dynamic) -> payload, -) -> Selector(payload) { - let handler = fn(message: #(tag, Dynamic, Dynamic, Dynamic)) { - transform(message.1, message.2, message.3) + case subject { + NamedSubject(name) -> remove_selector_handler(selector, #(name, 2)) + Subject(_, tag:) -> remove_selector_handler(selector, #(tag, 2)) } - insert_selector_handler(selector, #(tag, 4), handler) } -/// Add a handler to a selector for 5 element tuple messages with a given tag -/// element in the first position. +/// Add a handler to a selector for tuple messages with a given tag in the +/// first position followed by a given number of fields. /// -/// Typically you want to use the `selecting` function with a `Subject` instead, +/// Typically you want to use the `select` function with a `Subject` instead, /// but this function may be useful if you need to receive messages sent from /// other BEAM languages that do not use the `Subject` type. /// -pub fn selecting_record5( - selector: Selector(payload), - tag: tag, - mapping transform: fn(Dynamic, Dynamic, Dynamic, Dynamic) -> payload, -) -> Selector(payload) { - let handler = fn(message: #(tag, Dynamic, Dynamic, Dynamic, Dynamic)) { - transform(message.1, message.2, message.3, message.4) - } - insert_selector_handler(selector, #(tag, 5), handler) -} - -/// Add a handler to a selector for 6 element tuple messages with a given tag -/// element in the first position. +/// This will not select messages sent via a subject even if the message has +/// the same tag in the first position. This is because when a message is sent +/// via a subject a new tag is used that is unique and specific to that subject. /// -/// Typically you want to use the `selecting` function with a `Subject` instead, -/// but this function may be useful if you need to receive messages sent from -/// other BEAM languages that do not use the `Subject` type. -/// -pub fn selecting_record6( +pub fn select_record( selector: Selector(payload), - tag: tag, - mapping transform: fn(Dynamic, Dynamic, Dynamic, Dynamic, Dynamic) -> payload, -) -> Selector(payload) { - let handler = fn(message: #(tag, Dynamic, Dynamic, Dynamic, Dynamic, Dynamic)) { - transform(message.1, message.2, message.3, message.4, message.5) - } - insert_selector_handler(selector, #(tag, 6), handler) -} - -/// Add a handler to a selector for 7 element tuple messages with a given tag -/// element in the first position. -/// -/// Typically you want to use the `selecting` function with a `Subject` instead, -/// but this function may be useful if you need to receive messages sent from -/// other BEAM languages that do not use the `Subject` type. -/// -pub fn selecting_record7( - selector: Selector(payload), - tag: tag, - mapping transform: fn(Dynamic, Dynamic, Dynamic, Dynamic, Dynamic, Dynamic) -> - payload, -) -> Selector(payload) { - let handler = fn( - message: #(tag, Dynamic, Dynamic, Dynamic, Dynamic, Dynamic, Dynamic), - ) { - transform(message.1, message.2, message.3, message.4, message.5, message.6) - } - insert_selector_handler(selector, #(tag, 7), handler) -} - -/// Add a handler to a selector for 8 element tuple messages with a given tag -/// element in the first position. -/// -/// Typically you want to use the `selecting` function with a `Subject` instead, -/// but this function may be useful if you need to receive messages sent from -/// other BEAM languages that do not use the `Subject` type. -/// -pub fn selecting_record8( - selector: Selector(payload), - tag: tag, - mapping transform: fn( - Dynamic, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - ) -> - payload, + tag tag: tag, + fields arity: Int, + mapping transform: fn(Dynamic) -> payload, ) -> Selector(payload) { - let handler = fn( - message: #( - tag, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - Dynamic, - ), - ) { - transform( - message.1, - message.2, - message.3, - message.4, - message.5, - message.6, - message.7, - ) - } - insert_selector_handler(selector, #(tag, 8), handler) + insert_selector_handler(selector, #(tag, arity + 1), transform) } type AnythingSelectorTag { @@ -456,7 +425,7 @@ type AnythingSelectorTag { /// is handled, or when you need to handle messages from other BEAM languages /// which do not use subjects or record format messages. /// -pub fn selecting_anything( +pub fn select_other( selector: Selector(payload), mapping handler: fn(Dynamic) -> payload, ) -> Selector(payload) { @@ -503,16 +472,15 @@ type ProcessMonitorFlag { } @external(erlang, "erlang", "monitor") -fn erlang_monitor_process(a: ProcessMonitorFlag, b: Pid) -> Reference +fn erlang_monitor_process(a: ProcessMonitorFlag, b: Pid) -> Monitor -pub opaque type ProcessMonitor { - ProcessMonitor(tag: Reference) -} +pub type Monitor /// A message received when a monitored process exits. /// -pub type ProcessDown { - ProcessDown(pid: Pid, reason: Dynamic) +pub type Down { + ProcessDown(monitor: Monitor, pid: Pid, reason: ExitReason) + PortDown(monitor: Monitor, port: Port, reason: ExitReason) } /// Start monitoring a process so that when the monitored process exits a @@ -522,172 +490,191 @@ pub type ProcessDown { /// process was not alive when this function is called the message will never /// be received. /// -/// The down message can be received with a `Selector` and the -/// `selecting_process_down` function. +/// The down message can be received with a selector and the +/// `select_monitors` function. /// /// The process can be demonitored with the `demonitor_process` function. /// -pub fn monitor_process(pid: Pid) -> ProcessMonitor { - Process - |> erlang_monitor_process(pid) - |> ProcessMonitor +pub fn monitor(pid: Pid) -> Monitor { + erlang_monitor_process(Process, pid) +} + +/// Select for a message sent for a given monitor. +/// +/// Each monitor handler added to a selector has a select performance cost, +/// so prefer [`select_monitors`](#select_monitors) if you are select +/// for multiple monitors. +/// +/// The handler can be removed from the selector later using +/// [`deselect_specific_monitor`](#deselect_specific_monitor). +/// +pub fn select_specific_monitor( + selector: Selector(payload), + monitor: Monitor, + mapping: fn(Down) -> payload, +) { + insert_selector_handler(selector, monitor, mapping) } -/// Add a `ProcessMonitor` to a `Selector` so that the `ProcessDown` message can -/// be received using the `Selector` and the `select` function. The -/// `ProcessMonitor` can be removed later with -/// [`deselecting_process_down`](#deselecting_process_down). +/// Select for any messages sent for any monitors set up by the select process. +/// +/// If you want to select for a specific message then use +/// [`select_specific_monitor`](#select_specific_monitor), but this +/// function is preferred if you need to select for multiple monitors. /// -pub fn selecting_process_down( +pub fn select_monitors( selector: Selector(payload), - monitor: ProcessMonitor, - mapping: fn(ProcessDown) -> payload, + mapping: fn(Down) -> payload, ) -> Selector(payload) { - insert_selector_handler(selector, monitor.tag, mapping) + insert_selector_handler(selector, #(atom.create("DOWN"), 5), fn(message) { + mapping(cast_down_message(message)) + }) } +@external(erlang, "gleam_erlang_ffi", "cast_down_message") +fn cast_down_message(message: Dynamic) -> Down + +@external(erlang, "gleam_erlang_ffi", "cast_exit_reason") +fn cast_exit_reason(message: Dynamic) -> ExitReason + /// Remove the monitor for a process so that when the monitor process exits a -/// `ProcessDown` message is not sent to the monitoring process. +/// `Down` message is not sent to the monitoring process. /// /// If the message has already been sent it is removed from the monitoring /// process' mailbox. /// -pub fn demonitor_process(monitor monitor: ProcessMonitor) -> Nil { +pub fn demonitor_process(monitor monitor: Monitor) -> Nil { erlang_demonitor_process(monitor) Nil } @external(erlang, "gleam_erlang_ffi", "demonitor") -fn erlang_demonitor_process(monitor: ProcessMonitor) -> DoNotLeak - -/// An error returned when making a call to a process. -/// -pub type CallError(msg) { - /// The process being called exited before it sent a response. - /// - CalleeDown(reason: Dynamic) - - /// The process being called did not response within the permitted amount of - /// time. - /// - CallTimeout -} +fn erlang_demonitor_process(monitor: Monitor) -> DoNotLeak -/// Remove a `ProcessMonitor` from a `Selector` prevoiusly added by -/// [`selecting_process_down`](#selecting_process_down). If the `ProcessMonitor` is not in the -/// `Selector` it will be returned unchanged. +/// Remove a `Monitor` from a `Selector` prevoiusly added by +/// [`select_specific_monitor`](#select_specific_monitor). If +/// the `Monitor` is not in the `Selector` it will be returned +/// unchanged. /// -pub fn deselecting_process_down( +pub fn deselect_specific_monitor( selector: Selector(payload), - monitor: ProcessMonitor, + monitor: Monitor, ) -> Selector(payload) { - remove_selector_handler(selector, monitor.tag) + remove_selector_handler(selector, monitor) } -// This function is based off of Erlang's gen:do_call/4. -/// Send a message to a process and wait for a reply. -/// -/// If the receiving process exits or does not reply within the allowed amount -/// of time then an error is returned. -/// -/// The `within` parameter specifies the timeout duration in milliseconds. -/// -pub fn try_call( - subject: Subject(request), - make_request: fn(Subject(response)) -> request, - within timeout: Int, -) -> Result(response, CallError(response)) { +fn perform_call( + subject: Subject(message), + make_request: fn(Subject(reply)) -> message, + run_selector: fn(Selector(reply)) -> Result(reply, Nil), +) -> reply { let reply_subject = new_subject() + let assert Ok(callee) = subject_owner(subject) + as "Callee subject had no owner" // Monitor the callee process so we can tell if it goes down (meaning we // won't get a reply) - let monitor = monitor_process(subject_owner(subject)) + let monitor = monitor(callee) // Send the request to the process over the channel send(subject, make_request(reply_subject)) // Await a reply or handle failure modes (timeout, process down, etc) - let result = + let reply = new_selector() - |> selecting(reply_subject, Ok) - |> selecting_process_down(monitor, fn(down: ProcessDown) { - Error(CalleeDown(reason: down.reason)) + |> select(reply_subject) + |> select_specific_monitor(monitor, fn(down) { + panic as { "callee exited: " <> string.inspect(down) } }) - |> select(timeout) + |> run_selector + + let assert Ok(reply) = reply as "callee did not send reply before timeout" // Demonitor the process and close the channels as we're done demonitor_process(monitor) - // Prepare an appropriate error (if present) for the caller - case result { - Error(Nil) -> Error(CallTimeout) - Ok(res) -> res - } + reply } -/// Send a message to a process and wait for a reply. +// This function is based off of Erlang's gen:do_call/4. +/// Send a message to a process and wait a given number of milliseconds for a +/// reply. /// -/// If the receiving process exits or does not reply within the allowed amount -/// of time the calling process crashes. If you wish an error to be returned -/// instead see the `try_call` function. +/// ## Panics /// -/// The `within` parameter specifies the timeout duration in milliseconds. +/// This function will panic under the following circumstances: +/// - The callee process exited prior to sending a reply. +/// - The callee process did not send a reply within the permitted amount of +/// time. +/// - The subject is a named subject but no process is registered with that +/// name. /// -pub fn call( - subject: Subject(request), - make_request: fn(Subject(response)) -> request, - within timeout: Int, -) -> response { - let assert Ok(resp) = try_call(subject, make_request, timeout) - resp -} - -/// Similar to the `call` function but will wait forever for a message to -/// arrive rather than timing out after a specified amount of time. +/// ## Examples /// -/// If the receiving process exits, the calling process crashes. -/// If you wish an error to be returned instead see the `try_call_forever` -/// function. +/// ```gleam +/// pub type Message { +/// // This message variant is to be used with `call`. +/// // The `reply` field contains a subject that the reply message will be +/// // sent over. +/// SayHello(reply_to: Subject(String), name: String) +/// } +/// +/// // Typically we make public functions that hide the details of a process' +/// // message-based API. +/// pub fn say_hello(subject: Subject(Message), name: String) -> String { +/// // The `SayHello` message constructor is given _partially applied_ with +/// // all the arguments except the reply subject, which will be supplied by +/// // the `call` function itself before sending the message. +/// process.call(subject, 100, SayHello(_, name)) +/// } +/// +/// // This is the message handling logic used by the process that owns the +/// // subject, and so receives the messages. In a real project it would be +/// // within a process or some higher level abstraction like an actor, but for +/// // this demonstration that has been omitted. +/// pub fn handle_message(message: Message) -> Nil { +/// case message { +/// SayHello(reply:, name:) -> { +/// let data = "Hello, " <> name <> "!" +/// // The reply subject is used to send the response back. +/// // If the receiver process does not sent a reply in time then the +/// // caller will crash. +/// process.send(reply, data) +/// } +/// } +/// } +/// +/// // Here is what it looks like using the functional API to call the process. +/// pub fn run(subject: Subject(Message)) { +/// say_hello(subject, "Lucy") +/// // -> "Hello, Lucy!" +/// say_hello(subject, "Nubi") +/// // -> "Hello, Nubi!" +/// } +/// ``` /// -pub fn call_forever( - subject: Subject(request), - make_request: fn(Subject(response)) -> request, -) -> response { - let assert Ok(response) = try_call_forever(subject, make_request) - response +pub fn call( + subject: Subject(message), + waiting timeout: Int, + sending make_request: fn(Subject(reply)) -> message, +) -> reply { + perform_call(subject, make_request, selector_receive(_, timeout)) } -/// Similar to the `try_call` function but will wait forever for a message -/// to arrive rather than timing out after a specified amount of time. +/// Send a message to a process and wait for a reply. /// -/// If the receiving process exits then an error is returned. +/// # Panics /// -pub fn try_call_forever( - subject: Subject(request), - make_request: fn(Subject(response)) -> request, -) -> Result(response, CallError(c)) { - let reply_subject = new_subject() - - // Monitor the callee process so we can tell if it goes down (meaning we - // won't get a reply) - let monitor = monitor_process(subject_owner(subject)) - - // Send the request to the process over the channel - send(subject, make_request(reply_subject)) - - // Await a reply or handle failure modes (timeout, process down, etc) - let result = - new_selector() - |> selecting(reply_subject, Ok) - |> selecting_process_down(monitor, fn(down) { - Error(CalleeDown(reason: down.reason)) - }) - |> select_forever - - // Demonitor the process and close the channels as we're done - demonitor_process(monitor) - - result +/// This function will panic under the following circumstances: +/// - The callee process exited prior to sending a reply. +/// - The subject is a named subject but no process is registered with that +/// name. +/// +pub fn call_forever( + subject: Subject(message), + make_request: fn(Subject(reply)) -> message, +) -> reply { + perform_call(subject, make_request, fn(s) { Ok(selector_receive_forever(s)) }) } /// Creates a link between the calling process and another process. @@ -715,12 +702,18 @@ pub fn unlink(pid: Pid) -> Nil { pub type Timer @external(erlang, "erlang", "send_after") -fn erlang_send_after(a: Int, b: Pid, c: msg) -> Timer +fn pid_send_after(a: Int, b: Pid, c: #(Reference, msg)) -> Timer + +@external(erlang, "erlang", "send_after") +fn name_send_after(a: Int, b: Name(msg), c: #(Name(msg), msg)) -> Timer /// Send a message over a channel after a specified number of milliseconds. /// pub fn send_after(subject: Subject(msg), delay: Int, message: msg) -> Timer { - erlang_send_after(delay, subject.owner, #(subject.tag, message)) + case subject { + NamedSubject(name) -> name_send_after(delay, name, #(name, message)) + Subject(owner, tag) -> pid_send_after(delay, owner, #(tag, message)) + } } @external(erlang, "erlang", "cancel_timer") @@ -795,8 +788,8 @@ pub fn send_exit(to pid: Pid) -> Nil { /// /// [1]: http://erlang.org/doc/man/erlang.html#exit-2 /// -pub fn send_abnormal_exit(pid: Pid, reason: String) -> Nil { - erlang_send_exit(pid, Abnormal(reason)) +pub fn send_abnormal_exit(pid: Pid, reason: anything) -> Nil { + erlang_send_exit(pid, dynamic.from(reason)) Nil } @@ -808,7 +801,7 @@ pub fn send_abnormal_exit(pid: Pid, reason: String) -> Nil { /// /// When trapping exits (after this function is called) if a linked process /// crashes an exit message is sent to the process instead. These messages can -/// be handled with the `selecting_trapped_exits` function. +/// be handled with the `select_trapped_exits` function. /// @external(erlang, "gleam_erlang_ffi", "trap_exits") pub fn trap_exits(a: Bool) -> Nil @@ -820,10 +813,9 @@ pub fn trap_exits(a: Bool) -> Nil /// - The process for the pid no longer exists. /// - The name has already been registered. /// - The process already has a name. -/// - The name is the atom `undefined`, which is reserved by Erlang. /// @external(erlang, "gleam_erlang_ffi", "register_process") -pub fn register(pid: Pid, name: Atom) -> Result(Nil, Nil) +pub fn register(pid: Pid, name: Name(message)) -> Result(Nil, Nil) /// Un-register a process name, after which the process can no longer be looked /// up by that name, and both the name and the process can be re-used in other @@ -834,27 +826,9 @@ pub fn register(pid: Pid, name: Atom) -> Result(Nil, Nil) /// likely result in undesirable behaviour and crashes. /// @external(erlang, "gleam_erlang_ffi", "unregister_process") -pub fn unregister(name: Atom) -> Result(Nil, Nil) +pub fn unregister(name: Name(message)) -> Result(Nil, Nil) -/// Look up a process by name, returning the pid if it exists. +/// Look up a process by registered name, returning the pid if it exists. /// @external(erlang, "gleam_erlang_ffi", "process_named") -pub fn named(name: Atom) -> Result(Pid, Nil) - -/// Checks to see whether a `Dynamic` value is a pid, and return the pid if -/// it is. -/// -/// ## Examples -/// -/// ```gleam -/// import gleam/dynamic -/// pid_from_dynamic(dynamic.from(process.self())) -/// // -> Ok(process.self()) -/// ``` -/// -/// ```gleam -/// pid_from_dynamic(dynamic.from(123)) -/// // -> Error([DecodeError(expected: "Pid", found: "Int", path: [])]) -/// ``` -@external(erlang, "gleam_erlang_ffi", "pid_from_dynamic") -pub fn pid_from_dynamic(from from: Dynamic) -> Result(Pid, List(DecodeError)) +pub fn named(name: Name(a)) -> Result(Pid, Nil) diff --git a/src/gleam/erlang/reference.gleam b/src/gleam/erlang/reference.gleam new file mode 100644 index 0000000..8a69d64 --- /dev/null +++ b/src/gleam/erlang/reference.gleam @@ -0,0 +1,15 @@ +/// A unique reference value. +/// +/// It holds no particular meaning or value, but unique values are often useful +/// in programs are used heavily within both Gleam and Erlang's OTP frameworks. +/// +/// More can be read about references in the [Erlang documentation][1]. +/// +/// [1]: https://www.erlang.org/doc/efficiency_guide/advanced.html#unique_references +/// +pub type Reference + +/// Create a new unique reference. +/// +@external(erlang, "erlang", "make_ref") +pub fn new() -> Reference diff --git a/src/gleam_erlang_ffi.erl b/src/gleam_erlang_ffi.erl index 12ce581..b1ad710 100644 --- a/src/gleam_erlang_ffi.erl +++ b/src/gleam_erlang_ffi.erl @@ -1,66 +1,23 @@ -module(gleam_erlang_ffi). -export([ - atom_from_dynamic/1, rescue/1, atom_from_string/1, get_line/1, - ensure_all_started/1, sleep/1, os_family/0, sleep_forever/0, - get_all_env/0, get_env/1, set_env/2, unset_env/1, demonitor/1, - new_selector/0, link/1, insert_selector_handler/3, - remove_selector_handler/2, select/1, select/2, trap_exits/1, - map_selector/2, merge_selector/2, flush_messages/0, + atom_from_string/1, sleep/1, sleep_forever/0, demonitor/1, new_selector/0, + link/1, insert_selector_handler/3, remove_selector_handler/2, select/1, + select/2, trap_exits/1, map_selector/2, merge_selector/2, flush_messages/0, priv_directory/1, connect_node/1, register_process/2, unregister_process/1, - process_named/1, identity/1, pid_from_dynamic/1, reference_from_dynamic/1, - port_from_dynamic/1, 'receive'/1, 'receive'/2 + process_named/1, identity/1, 'receive'/1, 'receive'/2, new_name/1, + cast_down_message/1, cast_exit_reason/1 ]). --spec atom_from_string(binary()) -> {ok, atom()} | {error, atom_not_loaded}. +-spec atom_from_string(binary()) -> {ok, atom()} | {error, nil}. atom_from_string(S) -> try {ok, binary_to_existing_atom(S)} - catch error:badarg -> {error, atom_not_loaded} + catch error:badarg -> {error, nil} end. -atom_from_dynamic(Data) when is_atom(Data) -> - {ok, Data}; -atom_from_dynamic(Data) -> - {error, [{decode_error, <<"Atom">>, gleam@dynamic:classify(Data), []}]}. - -pid_from_dynamic(Data) when is_pid(Data) -> - {ok, Data}; -pid_from_dynamic(Data) -> - {error, [{decode_error, <<"Pid">>, gleam@dynamic:classify(Data), []}]}. - -reference_from_dynamic(Data) when is_reference(Data) -> - {ok, Data}; -reference_from_dynamic(Data) -> - {error, [{decode_error, <<"Reference">>, gleam@dynamic:classify(Data), []}]}. - -port_from_dynamic(Data) when is_port(Data) -> - {ok, Data}; -port_from_dynamic(Data) -> - {error, [{decode_error, <<"Port">>, gleam@dynamic:classify(Data), []}]}. - --spec get_line(io:prompt()) -> {ok, unicode:unicode_binary()} | {error, eof | no_data}. -get_line(Prompt) -> - case io:get_line(Prompt) of - eof -> {error, eof}; - {error, _} -> {error, no_data}; - Data when is_binary(Data) -> {ok, Data}; - Data when is_list(Data) -> {ok, unicode:characters_to_binary(Data)} - end. - -rescue(F) -> - try {ok, F()} - catch - throw:X -> {error, {thrown, X}}; - error:X -> {error, {errored, X}}; - exit:X -> {error, {exited, X}} - end. - -ensure_all_started(Application) -> - case application:ensure_all_started(Application) of - {ok, _} = Ok -> Ok; - - {error, {ProblemApp, {"no such file or directory", _}}} -> - {error, {unknown_application, ProblemApp}} - end. +new_name(Prefix) -> + Suffix = integer_to_binary(erlang:unique_integer([positive])), + Name = <>, + binary_to_atom(Name). sleep(Microseconds) -> timer:sleep(Microseconds), @@ -70,41 +27,6 @@ sleep_forever() -> timer:sleep(infinity), nil. -get_all_env() -> - BinVars = lists:map(fun(VarString) -> - [VarName, VarVal] = string:split(VarString, "="), - {list_to_binary(VarName), list_to_binary(VarVal)} - end, os:getenv()), - maps:from_list(BinVars). - -get_env(Name) -> - case os:getenv(binary_to_list(Name)) of - false -> {error, nil}; - Value -> {ok, list_to_binary(Value)} - end. - -set_env(Name, Value) -> - os:putenv(binary_to_list(Name), binary_to_list(Value)), - nil. - -unset_env(Name) -> - os:unsetenv(binary_to_list(Name)), - nil. - -os_family() -> - case os:type() of - {win32, nt} -> - windows_nt; - {unix, linux} -> - linux; - {unix, darwin} -> - darwin; - {unix, freebsd} -> - free_bsd; - {_, Other} -> - {other, atom_to_binary(Other, utf8)} - end. - new_selector() -> {selector, #{}}. @@ -133,9 +55,9 @@ select({selector, Handlers}, Timeout) -> % Monitored process down messages. % This is special cased so we can selectively receive based on the % reference as well as the record tag. - {'DOWN', Ref, process, Pid, Reason} when is_map_key(Ref, Handlers) -> + {'DOWN', Ref, _, _, _} = Message when is_map_key(Ref, Handlers) -> Fn = maps:get(Ref, Handlers), - {ok, Fn({process_down, Pid, Reason})}; + {ok, Fn(cast_down_message(Message))}; Msg when is_map_key({element(1, Msg), tuple_size(Msg)}, Handlers) -> Fn = maps:get({element(1, Msg), tuple_size(Msg)}, Handlers), @@ -147,21 +69,39 @@ select({selector, Handlers}, Timeout) -> {error, nil} end. +cast_exit_reason(normal) -> normal; +cast_exit_reason(killed) -> killed; +cast_exit_reason(Other) -> {abnormal, Other}. + +cast_down_message({'DOWN', Ref, process, Pid, Reason}) -> + {process_down, Ref, Pid, cast_exit_reason(Reason)}; +cast_down_message({'DOWN', Ref, port, Pid, Reason}) -> + {port_down, Ref, Pid, cast_exit_reason(Reason)}. + + 'receive'({subject, _Pid, Ref}) -> receive - {Ref, Message} -> - Message + {Ref, Message} -> Message + end; +'receive'({named_subject, Name}) -> + receive + {Name, Message} -> Message end. 'receive'({subject, _Pid, Ref}, Timeout) -> receive - {Ref, Message} -> - {ok, Message} + {Ref, Message} -> {ok, Message} + after Timeout -> + {error, nil} + end; +'receive'({named_subject, Name}, Timeout) -> + receive + {Name, Message} -> {ok, Message} after Timeout -> {error, nil} end. -demonitor({_, Reference}) -> +demonitor(Reference) -> erlang:demonitor(Reference, [flush]). link(Pid) -> diff --git a/test/gleam/erlang/atom_test.gleam b/test/gleam/erlang/atom_test.gleam index 710487f..7a469df 100644 --- a/test/gleam/erlang/atom_test.gleam +++ b/test/gleam/erlang/atom_test.gleam @@ -1,62 +1,24 @@ -import gleam/dynamic -import gleam/dynamic/decode.{DecodeError} import gleam/erlang/atom +import gleam/int pub fn from_string_test() { - let assert Ok(_) = atom.from_string("ok") - let assert Error(atom.AtomNotLoaded) = - atom.from_string("the vm does not have an atom with this content") + let assert Ok(_) = atom.get("ok") + let assert Error(Nil) = + atom.get("the vm does not have an atom with this content") } pub fn create_from_string_test() { - let result = - "ok" - |> atom.create_from_string - |> Ok - let assert True = result == atom.from_string("ok") - - let result = - "expect" - |> atom.create_from_string - |> Ok - let assert True = result == atom.from_string("expect") - - let result = - "this is another atom we have not seen before" - |> atom.create_from_string - |> Ok - let assert True = - result == atom.from_string("this is another atom we have not seen before") + let assert True = atom.get("ok") == Ok(atom.create("ok")) + + // Generate the string at runtime to prevent erlc from optimising the atom + // creation and doing it at compile time. + let new = "this is a new atom " <> int.to_string(int.random(100)) + let assert Error(Nil) = atom.get(new) + let created = atom.create(new) + let assert True = atom.get(new) == Ok(created) } pub fn to_string_test() { - let assert "ok" = atom.to_string(atom.create_from_string("ok")) - - let assert "expect" = atom.to_string(atom.create_from_string("expect")) -} - -pub fn from_dynamic_test() { - let result = - "" - |> atom.create_from_string - |> dynamic.from - |> atom.from_dynamic - let assert True = result == Ok(atom.create_from_string("")) - - let result = - "ok" - |> atom.create_from_string - |> dynamic.from - |> atom.from_dynamic - let assert True = result == Ok(atom.create_from_string("ok")) - - let assert Error([DecodeError(expected: "Atom", found: "Int", path: [])]) = - 1 - |> dynamic.from - |> atom.from_dynamic - - let assert Error([DecodeError(expected: "Atom", found: "List", path: [])]) = - [] - |> dynamic.from - |> atom.from_dynamic + let assert "ok" = atom.to_string(atom.create("ok")) + let assert "expect" = atom.to_string(atom.create("expect")) } diff --git a/test/gleam/erlang/node_tests.gleam b/test/gleam/erlang/node_tests.gleam index f98e68f..4c49ebd 100644 --- a/test/gleam/erlang/node_tests.gleam +++ b/test/gleam/erlang/node_tests.gleam @@ -14,10 +14,6 @@ pub fn visible_test() { } pub fn connect_not_alive_test() { - let name = atom.create_from_string("not_found@localhost") + let name = atom.create("not_found@localhost") let assert Error(node.LocalNodeIsNotAlive) = node.connect(name) } - -pub fn to_atom_test() { - let assert "nonode@nohost" = atom.to_string(node.to_atom(node.self())) -} diff --git a/test/gleam/erlang/port_test.gleam b/test/gleam/erlang/port_test.gleam deleted file mode 100644 index 390cdfb..0000000 --- a/test/gleam/erlang/port_test.gleam +++ /dev/null @@ -1,28 +0,0 @@ -import gleam/dynamic -import gleam/dynamic/decode.{DecodeError} -import gleam/erlang/port -import gleeunit/should - -type PortName { - Spawn(String) -} - -type PortSetting - -@external(erlang, "erlang", "open_port") -fn open_port( - port_name: PortName, - port_settings: List(PortSetting), -) -> dynamic.Dynamic - -pub fn port_dynamic_test() { - let msg = open_port(Spawn("echo \"hello world\""), []) - - msg - |> port.port_from_dynamic - |> should.be_ok() - - let assert Error([DecodeError(expected: "Port", found: "Int", path: [])]) = - dynamic.from(1) - |> port.port_from_dynamic -} diff --git a/test/gleam/erlang/process_test.gleam b/test/gleam/erlang/process_test.gleam index 3d8cf3f..85c3e20 100644 --- a/test/gleam/erlang/process_test.gleam +++ b/test/gleam/erlang/process_test.gleam @@ -1,21 +1,26 @@ import gleam/dynamic -import gleam/dynamic/decode.{DecodeError} +import gleam/dynamic/decode import gleam/erlang/atom import gleam/erlang/process.{ProcessDown} import gleam/float import gleam/function import gleam/int +import gleam/list import gleam/option.{Some} +import gleam/set import gleeunit/should +@external(erlang, "gleam_erlang_ffi", "identity") +fn unsafe_coerce(a: dynamic.Dynamic) -> anything + pub fn self_test() { let subject = process.new_subject() let pid = process.self() let assert True = pid == process.self() - let assert False = pid == process.start(fn() { Nil }, linked: True) + let assert False = pid == process.spawn(fn() { Nil }) - process.start(fn() { process.send(subject, process.self()) }, linked: True) + process.spawn(fn() { process.send(subject, process.self()) }) let assert Ok(child_pid) = process.receive(subject, 5) let assert True = child_pid != process.self() } @@ -27,7 +32,29 @@ pub fn sleep_test() { pub fn subject_owner_test() { let subject = process.new_subject() - let assert True = process.subject_owner(subject) == process.self() + let assert True = process.subject_owner(subject) == Ok(process.self()) +} + +pub fn new_name_test() { + let assert 1000 = + list.range(1, 1000) + |> list.map(fn(_) { process.new_name("name") }) + |> set.from_list + |> set.size +} + +pub fn subject_owner_named_test() { + let name = process.new_name("name") + let subject = process.named_subject(name) + let assert Ok(_) = process.register(process.self(), name) + let assert True = process.subject_owner(subject) == Ok(process.self()) + let assert Ok(_) = process.unregister(name) +} + +pub fn subject_owner_named_unregistered_test() { + let name = process.new_name("name") + let subject = process.named_subject(name) + let assert Error(Nil) = process.subject_owner(subject) } pub fn receive_test() { @@ -37,13 +64,10 @@ pub fn receive_test() { process.send(subject, 0) // Send message from another process - process.start( - fn() { - process.send(subject, 1) - process.send(subject, 2) - }, - linked: True, - ) + process.spawn(fn() { + process.send(subject, 1) + process.send(subject, 2) + }) // Assert all the messages arrived let assert Ok(0) = process.receive(subject, 0) @@ -59,13 +83,10 @@ pub fn receive_forever_test() { process.send(subject, 0) // Send message from another process - process.start( - fn() { - process.send(subject, 1) - process.send(subject, 2) - }, - linked: True, - ) + process.spawn(fn() { + process.send(subject, 1) + process.send(subject, 2) + }) // Assert all the messages arrived let assert 0 = process.receive_forever(subject) @@ -74,13 +95,13 @@ pub fn receive_forever_test() { } pub fn is_alive_test() { - let pid = process.start(fn() { Nil }, False) + let pid = process.spawn_unlinked(fn() { Nil }) process.sleep(5) let assert False = process.is_alive(pid) } pub fn sleep_forever_test() { - let pid = process.start(process.sleep_forever, False) + let pid = process.spawn_unlinked(process.sleep_forever) process.sleep(5) let assert True = process.is_alive(pid) } @@ -96,64 +117,128 @@ pub fn selector_test() { let selector = process.new_selector() - |> process.selecting(subject2, int.to_string) - |> process.selecting(subject3, float.to_string) + |> process.select_map(subject2, int.to_string) + |> process.select_map(subject3, float.to_string) // We can selectively receive messages for subjects 2 and 3, skipping the one // from subject 1 even though it is first in the mailbox. - let assert Ok("2") = process.select(selector, 0) - let assert Ok("3.0") = process.select(selector, 0) - let assert Error(Nil) = process.select(selector, 0) + let assert Ok("2") = process.selector_receive(selector, 0) + let assert Ok("3.0") = process.selector_receive(selector, 0) + let assert Error(Nil) = process.selector_receive(selector, 0) // More messages for subjects 2 and 3 process.send(subject2, 2) process.send(subject3, 3.0) // Include subject 1 also - let selector = process.selecting(selector, subject1, fn(x) { x }) + let selector = process.select(selector, subject1) // Now we get the message for subject 1 first as it is first in the mailbox - let assert Ok("1") = process.select(selector, 0) - let assert Ok("2") = process.select(selector, 0) - let assert Ok("3.0") = process.select(selector, 0) - let assert Error(Nil) = process.select(selector, 0) + let assert Ok("1") = process.selector_receive(selector, 0) + let assert Ok("2") = process.selector_receive(selector, 0) + let assert Ok("3.0") = process.selector_receive(selector, 0) + let assert Error(Nil) = process.selector_receive(selector, 0) +} + +pub fn monitor_normal_exit_test() { + monitor_process_exit(fn() { Nil }) + |> should.equal(process.Normal) +} + +pub fn monitor_killed_test() { + monitor_process_exit(fn() { process.kill(process.self()) }) + |> should.equal(process.Killed) } -pub fn monitor_test() { +pub fn monitor_abnormal_exit_test() { + monitor_process_exit(fn() { + process.send_abnormal_exit(process.self(), "reason") + }) + |> should.equal(process.Abnormal(dynamic.from("reason"))) +} + +/// Spawns a child, monitors exits, runs `terminating_with` in the child, +/// checks that a `ProcessDown` is received, and finally returns the exit reason. +fn monitor_process_exit(terminating_with: fn() -> Nil) -> process.ExitReason { // Spawn child let parent_subject = process.new_subject() let pid = - process.start(linked: False, running: fn() { + process.spawn_unlinked(fn() { let subject = process.new_subject() process.send(parent_subject, subject) // Wait for the parent to send a message before exiting - process.receive(subject, 150) + let assert Ok(_) = process.receive(subject, 150) + terminating_with() }) // Monitor child - let monitor = process.monitor_process(pid) + let monitor = process.monitor(pid) let selector = process.new_selector() - |> process.selecting_process_down(monitor, fn(x) { x }) + |> process.select_monitors(fn(x) { x }) // There is no monitor message while the child is alive - let assert Error(Nil) = process.select(selector, 0) + let assert Error(Nil) = process.selector_receive(selector, 0) - // Shutdown child to trigger monitor + // Terminate child to trigger monitor let assert Ok(child_subject) = process.receive(parent_subject, 50) process.send(child_subject, Nil) // We get a process down message! - let assert Ok(ProcessDown(downed_pid, _reason)) = process.select(selector, 50) + let assert Ok(ProcessDown(downed_monitor, downed_pid, reason)) = + process.selector_receive(selector, 50) let assert True = downed_pid == pid + let assert True = downed_monitor == monitor + + reason +} + +pub fn monitor_specific_test() { + let parent_subject = process.new_subject() + let spawn = fn() { + process.spawn_unlinked(fn() { + let subject = process.new_subject() + process.send(parent_subject, subject) + // Wait for the parent to send a message before exiting + process.receive(subject, 150) + }) + } + // Spawn child + let pid1 = spawn() + let pid2 = spawn() + + // Monitor children + let monitor1 = process.monitor(pid1) + let _monitor2 = process.monitor(pid2) + let selector = + process.new_selector() + |> process.select_specific_monitor(monitor1, fn(x) { x }) + + // There is no monitor message while the child is alive + let assert Error(Nil) = process.selector_receive(selector, 0) + + // Shutdown child to trigger monitor + let assert Ok(child_subject) = process.receive(parent_subject, 50) + process.send(child_subject, Nil) + + // We get a process down message! + let assert Ok(ProcessDown(downed_monitor, downed_pid, _reason)) = + process.selector_receive(selector, 50) + + let assert True = downed_pid == pid1 + let assert True = downed_monitor == monitor1 + + // We don't get the other one if we select again as the selector doesn't + // include it + let assert Error(Nil) = process.selector_receive(selector, 50) } pub fn demonitor_test() { // Spawn child let parent_subject = process.new_subject() let pid = - process.start(linked: False, running: fn() { + process.spawn_unlinked(fn() { let subject = process.new_subject() process.send(parent_subject, subject) // Wait for the parent to send a message before exiting @@ -161,11 +246,11 @@ pub fn demonitor_test() { }) // Monitor child - let monitor = process.monitor_process(pid) + let monitor = process.monitor(pid) let empty_selector = process.new_selector() let selector = empty_selector - |> process.selecting_process_down(monitor, fn(x) { x }) + |> process.select_specific_monitor(monitor, fn(x) { x }) // Shutdown child to trigger monitor let assert Ok(child_subject) = process.receive(parent_subject, 50) @@ -175,77 +260,17 @@ pub fn demonitor_test() { process.demonitor_process(monitor) // There is no down message - let assert Error(Nil) = process.select(selector, 5) + let assert Error(Nil) = process.selector_receive(selector, 5) // Remove monitor from selector let assert True = - empty_selector == selector |> process.deselecting_process_down(monitor) -} - -pub fn try_call_test() { - let parent_subject = process.new_subject() - - process.start(linked: True, running: fn() { - // Send the child subject to the parent so it can call the child - let child_subject = process.new_subject() - process.send(parent_subject, child_subject) - // Wait for the subject to be messaged - let assert Ok(#(x, reply)) = process.receive(child_subject, 50) - // Reply - process.send(reply, x + 1) - }) - - let assert Ok(call_subject) = process.receive(parent_subject, 50) - - // Call the child process and get a response. - let assert Ok(2) = - process.try_call(call_subject, fn(subject) { #(1, subject) }, 50) -} - -pub fn try_call_timeout_test() { - let parent_subject = process.new_subject() - - process.start(linked: True, running: fn() { - // Send the call subject to the parent - let child_subject = process.new_subject() - process.send(parent_subject, child_subject) - // Wait for the subject to be called - let assert Ok(_) = process.receive(child_subject, 50) - // Never reply - process.sleep(100) - }) - - let assert Ok(call_subject) = process.receive(parent_subject, 50) - - // Call the child process over the subject - let assert Error(process.CallTimeout) = - process.try_call(call_subject, fn(x) { x }, 10) -} - -pub fn try_call_forever_test() { - let parent_subject = process.new_subject() - - process.start(linked: True, running: fn() { - // Send the child subject to the parent so it can call the child - let child_subject = process.new_subject() - process.send(parent_subject, child_subject) - // Wait for the subject to be messaged - let assert Ok(#(x, reply)) = process.receive(child_subject, 50) - // Reply - process.send(reply, x + 1) - }) - - let assert Ok(call_subject) = process.receive(parent_subject, 50) - - // Call the child process and get a response. - let assert Ok(2) = - process.try_call_forever(call_subject, fn(subject) { #(1, subject) }) + empty_selector == selector |> process.deselect_specific_monitor(monitor) } pub fn call_test() { let parent_subject = process.new_subject() - process.start(linked: True, running: fn() { + process.spawn(fn() { // Send the child subject to the parent so it can call the child let child_subject = process.new_subject() process.send(parent_subject, child_subject) @@ -258,13 +283,13 @@ pub fn call_test() { let assert Ok(call_subject) = process.receive(parent_subject, 50) // Call the child process and get a response. - let assert 2 = process.call(call_subject, fn(subject) { #(1, subject) }, 50) + let assert 2 = process.call(call_subject, 50, fn(subject) { #(1, subject) }) } pub fn call_forever_test() { let parent_subject = process.new_subject() - process.start(linked: True, running: fn() { + process.spawn(fn() { // Send the child subject to the parent so it can call the child let child_subject = process.new_subject() process.send(parent_subject, child_subject) @@ -284,8 +309,9 @@ pub fn call_forever_test() { @external(erlang, "erlang", "send") fn send(a: process.Pid, b: anything) -> Nil -pub fn selecting_record_test() { +pub fn select_record_test() { send(process.self(), #("a", 1)) + send(process.self(), #("a", 1, 2)) send(process.self(), #("b", 2, 3)) send(process.self(), #("c", 4, 5, 6)) send(process.self(), #("d", 7, 8, 9, 10)) @@ -296,107 +322,82 @@ pub fn selecting_record_test() { let assert Error(Nil) = process.new_selector() - |> process.selecting_record2("h", unsafe_coerce) - |> process.select(0) + |> process.select_record("h", 1, unsafe_coerce) + |> process.selector_receive(0) let assert Error(Nil) = process.new_selector() - |> process.selecting_record2("c", unsafe_coerce) - |> process.select(0) + |> process.select_record("c", 1, unsafe_coerce) + |> process.selector_receive(0) let assert Error(Nil) = process.new_selector() - |> process.selecting_record2("c", unsafe_coerce) - |> process.select(0) + |> process.select_record("c", 1, unsafe_coerce) + |> process.selector_receive(0) let assert Error(Nil) = process.new_selector() - |> process.selecting_record3("c", fn(a, b) { - #(unsafe_coerce(a), unsafe_coerce(b)) - }) - |> process.select(0) + |> process.select_record("c", 2, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(#(22, 23, 24, 25, 26, 27, 28)) = + let assert Ok(#("g", 22, 23, 24, 25, 26, 27, 28)) = process.new_selector() - |> process.selecting_record8("g", fn(a, b, c, d, e, f, g) { - #( - unsafe_coerce(a), - unsafe_coerce(b), - unsafe_coerce(c), - unsafe_coerce(d), - unsafe_coerce(e), - unsafe_coerce(f), - unsafe_coerce(g), - ) - }) - |> process.select(0) + |> process.select_record("g", 7, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(#(16, 17, 18, 19, 20, 21)) = + let assert Ok(#("f", 16, 17, 18, 19, 20, 21)) = process.new_selector() - |> process.selecting_record7("f", fn(a, b, c, d, e, f) { - #( - unsafe_coerce(a), - unsafe_coerce(b), - unsafe_coerce(c), - unsafe_coerce(d), - unsafe_coerce(e), - unsafe_coerce(f), - ) - }) - |> process.select(0) + |> process.select_record("f", 6, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(#(11, 12, 13, 14, 15)) = + let assert Ok(#("e", 11, 12, 13, 14, 15)) = process.new_selector() - |> process.selecting_record6("e", fn(a, b, c, d, e) { - #( - unsafe_coerce(a), - unsafe_coerce(b), - unsafe_coerce(c), - unsafe_coerce(d), - unsafe_coerce(e), - ) - }) - |> process.select(0) + |> process.select_record("e", 5, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(#(7, 8, 9, 10)) = + let assert Ok(#("d", 7, 8, 9, 10)) = process.new_selector() - |> process.selecting_record5("d", fn(a, b, c, d) { - #(unsafe_coerce(a), unsafe_coerce(b), unsafe_coerce(c), unsafe_coerce(d)) - }) - |> process.select(0) + |> process.select_record("d", 4, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(#(4, 5, 6)) = + let assert Ok(#("c", 4, 5, 6)) = process.new_selector() - |> process.selecting_record4("c", fn(a, b, c) { - #(unsafe_coerce(a), unsafe_coerce(b), unsafe_coerce(c)) - }) - |> process.select(0) + |> process.select_record("c", 3, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(#(2, 3)) = + let assert Ok(#("b", 2, 3)) = process.new_selector() - |> process.selecting_record3("b", fn(a, b) { - #(unsafe_coerce(a), unsafe_coerce(b)) - }) - |> process.select(0) + |> process.select_record("b", 2, unsafe_coerce) + |> process.selector_receive(0) + + let assert Ok(#("a", 1)) = + process.new_selector() + |> process.select_record("a", 1, unsafe_coerce) + |> process.selector_receive(0) - let assert Ok(1) = + let assert Error(Nil) = + process.new_selector() + |> process.select_record("a", 1, unsafe_coerce) + |> process.selector_receive(0) + + let assert Ok(#("a", 1, 2)) = process.new_selector() - |> process.selecting_record2("a", unsafe_coerce) - |> process.select(0) + |> process.select_record("a", 2, unsafe_coerce) + |> process.selector_receive(0) } -pub fn selecting_anything_test() { +pub fn select_anything_test() { process.flush_messages() send(process.self(), 1) send(process.self(), 2.0) let selector = process.new_selector() - |> process.selecting_anything(decode.run(_, decode.int)) + |> process.select_other(decode.run(_, decode.int)) - let assert Ok(Ok(1)) = process.select(selector, 0) + let assert Ok(Ok(1)) = process.selector_receive(selector, 0) let assert Ok(Error([ decode.DecodeError(expected: "Int", found: "Float", path: []), - ])) = process.select(selector, 0) - let assert Error(Nil) = process.select(selector, 0) + ])) = process.selector_receive(selector, 0) + let assert Error(Nil) = process.selector_receive(selector, 0) } pub fn linking_self_test() { @@ -405,38 +406,29 @@ pub fn linking_self_test() { pub fn linking_new_test() { let assert True = - process.link( - process.start(linked: False, running: fn() { process.sleep(100) }), - ) + process.link(process.spawn_unlinked(fn() { process.sleep(100) })) } pub fn relinking_test() { - let assert True = - process.link( - process.start(linked: True, running: fn() { process.sleep(100) }), - ) + let assert True = process.link(process.spawn(fn() { process.sleep(100) })) } pub fn linking_dead_test() { - let pid = process.start(linked: True, running: fn() { Nil }) + let pid = process.spawn(fn() { Nil }) process.sleep(20) let assert False = process.link(pid) } pub fn unlink_unlinked_test() { - process.unlink( - process.start(linked: False, running: fn() { process.sleep(100) }), - ) + process.unlink(process.spawn_unlinked(fn() { process.sleep(100) })) } pub fn unlink_linked_test() { - process.unlink( - process.start(linked: True, running: fn() { process.sleep(100) }), - ) + process.unlink(process.spawn(fn() { process.sleep(100) })) } pub fn unlink_dead_test() { - let pid = process.start(linked: True, running: fn() { Nil }) + let pid = process.spawn(fn() { Nil }) process.sleep(10) process.unlink(pid) } @@ -469,38 +461,38 @@ pub fn cancel_already_fired_timer_test() { } pub fn kill_test() { - let pid = process.start(linked: False, running: fn() { process.sleep(100) }) + let pid = process.spawn_unlinked(fn() { process.sleep(100) }) let assert True = process.is_alive(pid) process.kill(pid) let assert False = process.is_alive(pid) } pub fn kill_already_dead_test() { - let pid = process.start(linked: True, running: fn() { Nil }) + let pid = process.spawn(fn() { Nil }) process.sleep(10) let assert False = process.is_alive(pid) process.kill(pid) } pub fn send_exit_test() { - let pid = process.start(linked: False, running: fn() { process.sleep(100) }) + let pid = process.spawn_unlinked(fn() { process.sleep(100) }) process.send_exit(pid) } pub fn send_exit_already_dead_test() { - let pid = process.start(linked: True, running: fn() { Nil }) + let pid = process.spawn(fn() { Nil }) process.sleep(10) let assert False = process.is_alive(pid) process.send_exit(pid) } pub fn send_abnormal_exit_test() { - let pid = process.start(linked: False, running: fn() { process.sleep(100) }) + let pid = process.spawn_unlinked(fn() { process.sleep(100) }) process.send_abnormal_exit(pid, "Bye") } pub fn send_abnormal_exit_already_dead_test() { - let pid = process.start(linked: True, running: fn() { Nil }) + let pid = process.spawn(fn() { Nil }) process.sleep(10) let assert False = process.is_alive(pid) process.send_abnormal_exit(pid, "Bye") @@ -508,7 +500,7 @@ pub fn send_abnormal_exit_already_dead_test() { pub fn trap_exit_test() { process.trap_exits(True) - let pid = process.start(linked: True, running: fn() { process.sleep(100) }) + let pid = process.spawn(fn() { process.sleep(100) }) // This would cause an error if we were not trapping exits process.kill(pid) } @@ -519,8 +511,8 @@ pub fn select_forever_test() { let assert 1 = process.new_selector() - |> process.selecting(subject, function.identity) - |> process.select_forever + |> process.select(subject) + |> process.selector_receive_forever } pub fn map_selector_test() { @@ -531,12 +523,12 @@ pub fn map_selector_test() { let selector = process.new_selector() - |> process.selecting(subject1, int.to_string) - |> process.selecting(subject2, float.to_string) + |> process.select_map(subject1, int.to_string) + |> process.select_map(subject2, float.to_string) |> process.map_selector(Some) - let assert Some("1") = process.select_forever(selector) - let assert Some("2.0") = process.select_forever(selector) + let assert Some("1") = process.selector_receive_forever(selector) + let assert Some("2.0") = process.selector_receive_forever(selector) } pub fn merge_selector_test() { @@ -547,30 +539,50 @@ pub fn merge_selector_test() { let selector = process.new_selector() - |> process.selecting(subject1, fn(a) { #("a", a) }) - |> process.selecting(subject2, fn(a) { #("a", a) }) + |> process.select_map(subject1, fn(a) { #("a", a) }) + |> process.select_map(subject2, fn(a) { #("a", a) }) |> process.merge_selector( process.new_selector() - |> process.selecting(subject2, fn(a) { #("b", a) }), + |> process.select_map(subject2, fn(a) { #("b", a) }), ) - let assert #("a", 1) = process.select_forever(selector) - let assert #("b", 2) = process.select_forever(selector) + let assert #("a", 1) = process.selector_receive_forever(selector) + let assert #("b", 2) = process.selector_receive_forever(selector) +} + +pub fn select_trapped_exits_kill_test() { + select_trapped_exits(fn() { process.kill(process.self()) }) + |> should.equal(process.Killed) +} + +pub fn select_trapped_exits_abnormal_test() { + select_trapped_exits(fn() { + process.send_abnormal_exit(process.self(), "reason") + }) + |> should.equal(process.Abnormal(dynamic.from("reason"))) } -pub fn selecting_trapped_exits_test() { +pub fn select_trapped_exits_normal_test() { + select_trapped_exits(fn() { Nil }) + |> should.equal(process.Normal) +} + +/// Traps exits, starts a linked child, runs `terminating_with` in the child, +/// expects an `ExitMessage`, and returns the exit reason +pub fn select_trapped_exits(terminating_with: fn() -> Nil) -> process.ExitReason { process.flush_messages() process.trap_exits(True) - let pid = process.start(linked: True, running: fn() { process.sleep(100) }) - process.kill(pid) + let pid = process.spawn(terminating_with) - let assert Ok(process.ExitMessage(exited, process.Killed)) = + let assert Ok(process.ExitMessage(exited, reason)) = process.new_selector() - |> process.selecting_trapped_exits(function.identity) - |> process.select(10) + |> process.select_trapped_exits(function.identity) + |> process.selector_receive(10) let assert True = pid == exited + process.trap_exits(False) + reason } pub fn flush_messages_test() { @@ -583,7 +595,7 @@ pub fn flush_messages_test() { } pub fn register_name_taken_test() { - let taken_name = atom.create_from_string("code_server") + let taken_name = unsafe_coerce(dynamic.from(atom.create("code_server"))) let assert Ok(a) = process.named(taken_name) let assert Error(Nil) = process.register(process.self(), taken_name) let assert Ok(b) = process.named(taken_name) @@ -591,7 +603,7 @@ pub fn register_name_taken_test() { } pub fn register_name_test() { - let name = atom.create_from_string("register_name_test_name") + let name = process.new_name("name") let _ = process.unregister(name) let assert Error(Nil) = process.named(name) let assert Ok(Nil) = process.register(process.self(), name) @@ -601,7 +613,7 @@ pub fn register_name_test() { } pub fn unregister_name_test() { - let name = atom.create_from_string("unregister_name_test_name") + let name = process.new_name("name") let _ = process.unregister(name) let assert Ok(Nil) = process.register(process.self(), name) let assert Ok(_) = process.named(name) @@ -610,44 +622,32 @@ pub fn unregister_name_test() { let _ = process.unregister(name) } -pub fn pid_from_dynamic_test() { - let result = - process.self() - |> dynamic.from - |> process.pid_from_dynamic - let assert True = result == Ok(process.self()) - - let assert Error([DecodeError(expected: "Pid", found: "Int", path: [])]) = - 1 - |> dynamic.from - |> process.pid_from_dynamic - - let assert Error([DecodeError(expected: "Pid", found: "List", path: [])]) = - [] - |> dynamic.from - |> process.pid_from_dynamic -} - -pub fn deselecting_test() { +pub fn deselect_test() { let subject1 = process.new_subject() let subject2 = process.new_subject() let selector0 = process.new_selector() - let selector1 = selector0 |> process.selecting(subject1, function.identity) - let selector2 = selector1 |> process.selecting(subject2, function.identity) + let selector1 = selector0 |> process.select(subject1) + let selector2 = selector1 |> process.select(subject2) selector2 - |> process.deselecting(subject2) + |> process.deselect(subject2) |> should.equal(selector1) selector1 - |> process.deselecting(subject1) + |> process.deselect(subject1) |> should.equal(selector0) selector2 - |> process.deselecting(subject1) - |> process.deselecting(subject2) + |> process.deselect(subject1) + |> process.deselect(subject2) |> should.equal(selector0) } -@external(erlang, "gleam_erlang_ffi", "identity") -fn unsafe_coerce(a: dynamic.Dynamic) -> anything +pub fn name_test() { + let name = process.new_name("name") + let assert Ok(_) = process.register(process.self(), name) + let subject = process.named_subject(name) + process.send(subject, "Hello") + let assert Ok("Hello") = process.receive(subject, 0) + process.unregister(name) +} diff --git a/test/gleam/erlang_test.gleam b/test/gleam/erlang_test.gleam deleted file mode 100644 index 8de5619..0000000 --- a/test/gleam/erlang_test.gleam +++ /dev/null @@ -1,93 +0,0 @@ -import gleam/dynamic -import gleam/dynamic/decode.{DecodeError} -import gleam/erlang.{UnknownApplication} -import gleam/erlang/atom -import gleam/list -import gleam/string - -pub fn term_to_binary_test() { - let term = dynamic.from(#(1, "2", <<"hello":utf8>>)) - - let assert Ok(out) = - term - |> erlang.term_to_binary() - |> erlang.binary_to_term() - let assert True = term == out - - let assert Ok(out) = - term - |> erlang.term_to_binary() - |> erlang.unsafe_binary_to_term() - let assert True = term == out - - let assert Error(Nil) = - <<>> - |> erlang.unsafe_binary_to_term() -} - -pub fn ensure_all_started_ok_test() { - let inets = atom.create_from_string("inets") - let assert Ok([app1]) = erlang.ensure_all_started(inets) - let assert True = app1 == inets - - // If they are already started then empty list is returned - let assert Ok([]) = erlang.ensure_all_started(inets) -} - -pub fn ensure_all_started_unknown_test() { - let unknown = atom.create_from_string("wibble_application") - let assert Error(UnknownApplication(problem)) = - erlang.ensure_all_started(unknown) - let assert True = problem == unknown -} - -pub fn make_reference_test() { - let reference = erlang.make_reference() - list.range(0, 100_000) - |> list.each(fn(_) { - let assert True = reference != erlang.make_reference() - }) -} - -pub fn reference_from_dynamic_test() { - let reference = erlang.make_reference() - let assert Ok(reference_from_dynamic) = - reference - |> dynamic.from - |> erlang.reference_from_dynamic - let assert True = reference == reference_from_dynamic - - let assert Error([DecodeError(expected: "Reference", found: "Int", path: [])]) = - 123 - |> dynamic.from - |> erlang.reference_from_dynamic - - let assert Error([ - DecodeError(expected: "Reference", found: "String", path: []), - ]) = - "abc" - |> dynamic.from - |> erlang.reference_from_dynamic -} - -pub fn priv_directory_test() { - let assert Error(Nil) = erlang.priv_directory("unknown_application") - - let assert Ok(dir) = erlang.priv_directory("gleam_erlang") - let assert True = string.ends_with(dir, "/gleam_erlang/priv") - - let assert Ok(dir) = erlang.priv_directory("gleam_stdlib") - let assert True = string.ends_with(dir, "/gleam_stdlib/priv") -} - -pub fn bounded_phash2_test() { - let assert 9 = erlang.bounded_phash2("hello", limit: 10) - let assert 0 = erlang.bounded_phash2([5, 2, 8], limit: 10) - let assert 82 = erlang.bounded_phash2(Ok(#("testing", 123)), limit: 200) -} - -pub fn phash2_test() { - let assert 47_480_723 = erlang.phash2("hello") - let assert 79_761_634 = erlang.phash2([5, 2, 8]) - let assert 133_777_698 = erlang.phash2(Ok(#("testing", 123))) -}