Introduction

Orchid is an embeddable declarative programming language. It can be thought of as an extension of a configuration format that supports functions and complex logic. In this regard it occupies a similar niche to Lua.

Because the language is fully sandboxed and all interaction with the external world is defined by the embedder, standard I/O is rather unimportant, so this guide focuses on demonstrating the internal logic of the language and I/O facilities are only explained as and when they are needed to demonstrate a concept.

If you want to provide feedback on these docs, open an issue at github.com/lbfalvy/orchid-reference.

Getting started

In order to start experimenting with Orchid, you need a Rust toolchain. Instructions on how to install Rust on your system are available at www.rust-lang.org/tools/install.

Once you have a stable Rust toolchain installed, you can clone the repository:

git clone https://github.com/lbfalvy/orchid

/examples contains some demo projects that you can refer to while experimenting, this is also a great place to put your own experiments. When you start the interpreter with cargo run --release, it starts from main.orc in the current directory.

It is possible to install the interpreter system-wide by running cargo build --release and then adding target/release/orcx which is a standalone executable to $PATH, but because the language is still in heavy development and bugfixes and major features are added regularly it is easier to work in the repo and update the interpreter with a simple git fetch && git pull.

Getting started

In order to start experimenting with Orchid, you need a Rust toolchain. Instructions on how to install Rust on your system are available at www.rust-lang.org/tools/install.

Once you have a stable Rust toolchain installed, you can clone the repository:

git clone https://github.com/lbfalvy/orchid

/examples contains some demo projects that you can refer to while experimenting, this is also a great place to put your own experiments. When you start the interpreter with cargo run --release, it starts from main.orc in the current directory.

It is possible to install the interpreter system-wide by running cargo build --release and then adding target/release/orcx which is a standalone executable to $PATH, but because the language is still in heavy development and bugfixes and major features are added regularly it is easier to work in the repo and update the interpreter with a simple git fetch && git pull.

Hello World

Orchid programs are made up of expressions, and the only effect expressions have is their value. The value of the whole program is printed by the interpreter. As such, the Hello World program in Orchid is as simple as:

-- in main.orc
const main := "Hello World!"

This should print i"Hello World!" on the terminal. The i stands for interned. Orchid strings can be interned and literals are interned by default. Equality comparison between interned strings is very fast. This optimization is a cornerstone of Javascript's performance.

Comment syntax is inspired by Lua; line comments start with -- and stretch until the end of the line, while block comments start with --[ and end with ]--.

Constants are written as const <name> := <expression> terminated by a newline, and they allow you to extract subexpressions for brevity. If you add parentheses around an expression its meaning doesn't change, so long constants can be wrapped in parentheses and broken to multiple lines. The value of the program is the value of the constant main.

Other than line breaks ending semantic items when they appear outside parentheses, whitespace is never significant.

Arithmetic and Logic

Numbers

Basic arithmetic follows C-style syntax with infix operators and operator precedence:

const main := 2 + 2 * (3 + 4)

Orchid internally uses two number types; nonnegative platform-size integers represented with Rust's usize, and 64-bit floating point numbers that can never be NaN.

Number literals may be expressed in decimal, hex with 0x, binary with 0b and octal with 0o prefix. Each of these may contain a decimal point and/or a decimal exponent separated by a letter p representing a power of the base. For example, 0xF.Fp11 means 0xFF00_0000_0000. Number constants may contain underscores anywhere after the radix prefix for readability.

A number constant describes an integer if it doesn't have a decimal point and either doesn't have an exponent or the exponent is positive and not too large for a valid platform integer.

Multiplying, summing and taking module of integers results in an integer, all other operations return a float, which can be floored and cast to an usize with std::to_uint, like so:

import std::to_uint

const main := to_uint 4.5

Booleans

Branching is done with the ternary operator if/then/else, which also has a return value:

const a := 2
const b := 3
const main := if a < b then b else a

The standard infix relational operators ==, !=, <, >, <=, >= work on numbers. The infix operators and, or, and the function not can be used to combine booleans.

Strings

Strings are enclosed in double quotes, support line breaks and common escape sequences. In addition, an escaped line break and the subsequent indentation are ignored:

-- prints i"foo and bar"
const main := "foo and \
               bar"

Strings support a variety of methods described in std::string

numbers can be stringified with std::to_string, strings can be parsed to numbers via std::to_uint or std::to_float respectively. All three functions accept their return type and pass it through

Functions

Function calls in orchid are the most fundamental step and do not use an operator. Functions are applied to arguments by simply writing the arguments after the function name.

import std::to_string

const main := to_string 3.14
-- prints r"3.14"

The "r" in the result of the above program indicates that the string is not interned.

Functions are first-class values, so the equivalent of defining a function in a procedural language is simply to assign a constant. Functions are written as "\ argname . body"

const square := \x. x * x
const main := square 5
-- prints 25

All functions take a single argument, multi-parameter functions are represented by returning a function. This is called currying. The technique is most well known from Haskell.

const add_sqrs := \a. \b. a * a + b * b
const main := add_sqrs 3 5
-- prints 34

This also means that all parameters don't have to be provided at the same time. It's possible to store functions with some arguments missing.

const linear := \a. \b. \x. a * x + b
const scale2add3 := linear 2 3
const main := scale2add3 5
-- prints 13

We advise against doing this in most cases however as changes to the argument set may have unexpectedly deep architectural implications.

When the result of a function call is passed as the argument of another, the nested function call must be wrapped in parentheses:

import std::to_string

const sqr := \a. a * a
const main := to_string (sqr 5)
-- prints r"25"

Without the parens, this would attempt to stringify sqr and then pass 5 to the resulting string as though it was a function.

Modules and visibility.

Each folder and file is a module. In addition, submodules can be defined within files with the syntax module <name> ( <lines> ). Constants and submodules defined in a file are private by default and can be exported with the export prefix.

-- in tool.orc
module implementation (
  const hidden_data := "foo"
  export const some_operation := \s. s ++ hidden_data
)

export module public (
  import super::implementation

  export const do_stuff := implementation::some_operation
)
-- in main.orc
import tool::public::do_stuff

const main := do_stuff "bar"

super at the beginning of an import path points to the enclosing module, much like it does in Rust.

Control flow

There are various convenient syntax structures in the standard library to make working with expressions more convenient. The most common of them is the do block, which enables us to bind names to subexpressions, and also presents a modular framework other modules can use to interact with control flow. What this means exactly will be shown in later sections.

Notice also that the last line in the do block is an expression. Every orchid expression must have a value, and the expression do{} desugars to is no exception.

const main := do {
  let foo = 12;
  let bar = foo * (foo - 1);
  to_string bar
}

Expressions are evaluated when their value is used, and there isn't any way to update the value bound to names, so the primary mechanism for looping is recursion. There are looping constructs in the standard library which abstract away the act of recursion so in practice the perogrammer rarely needs to consider this fact.

const main := do {
  let i = 5;
  while i > 0 (i, prod=1) {
    let prod = prod * i;
    let i = i - 1;
  };
  prod
}

Side Effects

Orchid is a pure functional language, so side effects cannot arise from the evaluation of functions. Instead, some functions return commands which can be executed by returning them as the value of the entire program. Of course, this way a program could only do one thing, so the commands also store the rest of the program; an expression to evaluate after the command has exacted its effect.

import system::io::*
import std::exit_status

const main := (
  print "Hello, user! what is your name?" (
    readln \name.
      println ("Goodbye, " ++ name ++ "!") (
        exit_status::success
      )
  )
)

In the above example, print and println take a third argument which is evaluated after the text is printed, and readln's argument is a callback which is passed the newly read string. The values of all of these are treated the same as the value of the main program itself, forming a linked list of commands. This method of defining operations is called continuation passing style.

std::exit_code::success is simply a custom constant that isn't printed by the interpreter. Since all Orchid programs must have a value, println must take a third argument and any other value would be debug-printed by the interpreter. std::exit_code::failure does the same thing except it causes the interpreter to exit with code 1.

cps

Ideally we would like to avoid writing code like the above example, which is reminiscent of early Node.JS APIs commonly referred to as callback hell. To address this, do blocks support a statement type prefixed with cps, shorthand for continuation-passing style.

let <name> = <expression>; <rest of block> approximately desugars to (\<name>. <rest of block>) (<expression>). This binds the value to the name in the context of the rest of the block. In contrast, cps passes the rest of the block to the expression, where it is assumed to fill the place of the continuation in an effectful command. This statement can bind not just one but zero or multiple names, corresponding to the number of arguments passed to the continuation by the expression.

import system::io::*
import std::exit_status

const main := do {
  cps print "Hello, user! what is your name? ";
  cps name = readln;
  cps println ("Goodbye, " ++ name ++ "!");
  exit_status::success
}

Given how fundamental cps is, it can be combined with a lot of utilities from std::cunctional. For example, cps foo = pass 1; will assign 1 to foo and continue the program directly. cps foo, bar = pass2 1 2; does the same thing for two values. cps identity; is a no-op, and cps return 2; sets the value of the whole enclosing block to 2, discarding the following statements.

These primitves are most useful in situations like branching. If an if/then/else expression picks between commands, it must be called with cps, but this means that both branches must accept a continuation. If one branch is pure and the other is effectful, wrapping the pure branch in pass, pass2 or identity can easily transform it to cps. return is useful as a break statement in some of the control structures exposed in std::loop

The final barrier to unbounded composition is do itself; although we can mix CPS and parametric statements within a block, the block itself still resolves to a single value, so injecting a do block containing CPS operations in another is not possible, since the continuation of the outer block is passed to the first command of the inner block. If we imagine a simplistic operation sequence (op1 (op2 (op3))), then putting this in a cps statement followed by op4 would produce (op1 (op2 (op3)) op4) and not (op1 (op2 (op3 (op4)))). This is really bad, op1 doesn't even accept two arguments! To fix this, we introduce another block format do cps { <statements> } which desugars to \cont. do { <statements> ; cont }. This is itself a valid cps statement, so it can safely be returned in branches of cps expressions. Note also that, unlike a do block, a do cps block doesn't end in an expression.

import system::io::*
import std::exit_status

const main := do {
  cps print "access: ";
  cps name = readln;
  cps if name == "user"
    then println "welcome, uwer!"
    else if name == "admin"
    then do cps {
      cps print "password: ";
      cps password = readln;
      cps if password == "Passw0rd!"
        then println "welcome, admin!"
        else identity
    }
    else identity;
  exit_status::success
}

IO

I/O is completely asynchronous. Each I/O operation takes the same 3 final arguments; success, error, continuation. The continuation is evaluated synchronously immediately after the command is received, and can be used to schedule multiple concurrent I/O operations. For the final I/O operation in a given command the continuation must be set to system::async::yield which either pops a task from the queue or blocks in wait if there are no pending tasks. yield does not accept a continuation, and in a block it should not be called with cps. It is the final value at the end of a block.

Success is the next command to execute if the task at hand has no result, otherwise it's a function mapping the result to the next command. The failure is a function from the error data to the next command. In the future the error data will probably be a std::map, but right now it's just the integer 0. The continuation is a command.

The STL wraps stream I/O to define print, println and readln. Since the default layout of three callbacks doesn't really lend itself to CPS, it's a good idea to define similar wrappers for sequences of I/O operations that are used together in your project.

Standard Library

Binary

These functions work with binaries, represented as a sequence of bytes.

concat binary -> binary -> binary
Appends a binary blob to another

slice binary -> usize -> usize -> binary
Copies out a subsection of the binary into a new blob specified by starting point and length

find binary -> binary -> option usize
Return the index where the second binary appears as a subsection of the first

split binary -> usize -> tuple[binary, binary]
Splits the binary into two halves at the given byte index

int_bytes usize
The length of a usize in bytes.

get_num binary -> usize -> usize -> bool -> usize
Takes a binary, a starting point, a length no greater than int_bytes, and a boolean indicating whether the encoding is little endian or not. Reads a usize from the specified location in the binary.

from_num usize -> bool -> usize -> binary
Takes a length no greater than int_bytes, a little endian flag and a number to encode. Turns the least significant length bytes of the given usize in the specified endianness into a binary.

size binary -> usize
Returns the number of bytes in a binary

Exit Status

Return values for programs that suppress the printing of the final value. Returning one of these signals an orderly exit from a program that handles its own I/O.

success cmd
Constant indicating that the program had successfully completed its task.

failure cmd
Constant indicating that a problem occurred. Causes exit code 1 to be emitted on Unix.

is_success cmd -> bool
Returns true for success, false for failure.

Functional

Primitives and operators for functional programming style.

identity T -> T
Returns its argument without modification

pass T -> (T -> U) -> U
Calls its second argument on its first argument.

pass2 T -> U -> (T -> U -> V) -> V
Calls its third argument on its first two arguments in unchanged order

return T -> U -> T
Discards its second argument and returns the first

$ operator
An operator borrowed from Haskell; it wraps the following tokens in parentheses to avoid accumulating closing parens in deeply nested sequences

to_string $ to_uint 3.14

|> operator
An operator borrowed from Elixir; it passes the left hand side as the first argument of the function, pushing all subsequent arguments back.

list::new[1, 2, 3, 4]
  |> list::filter (\x. x % 2 == 0)
  |> list::map (\x. x * 2)

() => syntax
An alternative spelling of a function which has a lower priority than do{} blocks and doesn't have to be parenthesized to appear in them. Because it binds a name it still has to be higher priority than |> which is a simple operator, but this can improve ergonomics for some use cases involving callbacks.

const square := (x) => x * x
const square_sum := (a, b) => square a + square b
const square_double := x => square_sum x x
-- the parens are optional for unary functions

List

A linked list data structure.

cons T -> list T -> list T
Adds an element to the head of the list

end list ?
Empty list

pop list T -> U -> (T -> list T -> U) -> U
Takes a list, a default value and a callback. Returns the default value if the list is empty, otherwise passes the head and tail to the callback.

fold list T -> U -> (U -> T -> U) -> U
Fold each element into an accumulator using a callback. This evaluates the entire list.

rfold list T -> U -> (U -> T -> U) -> U
Fold each element into an accumulator in reverse order. This evaulates the entire list, and isn't tail recursive.

reduce list T -> (T -> T -> T) -> option T
Fold each element into a shared element. This evaluates the entire list.

filter list T -> (T -> bool) -> list T
Return a new list that contains only the elements from the input list for which the function returns true.

map list T -> (T -> U) -> list U
Transform each element of the list.

skip list T -> usize -> list T
Skip N elements from the list and return the tail.

take list T -> usize -> list T
Return n elements from the list and discard the rest.

get list T -> usize -> option T
Return the nth element from the list.

enumerate list T -> list tuple[usize, T]
Map every element to a pair of the index and the original element

chain list (T -> T) -> T -> T
Chains a list of functions in revverse, calling the head of the list on the return value of the next element when called on the element after it and so on. The last element in the list receives the second argument.

The main use case is to turn a list of CPS commands into a single CPS command which executes all of them in order.

new[] expression
Creates a new list from a comma-separated sequence of elements.

const my_list := list::new[1, 2, "foo", 3.14, 22 / 7]

Loop

Exposes a variety of macros related to recursive looping. These rely in various ways on the blocks defined by std::procedural

loop_over syntax
This macro accepts a list of loop variables which may be initialized inline as the example shows, and the body of a do{} block except for the final expression. Between iterations of the loop only the loop variables are retained. Remember that these are still not mutable variables, so updates to their values are still done with let bindings.

const factorial := \a. loop_over (a, b=1) {
  let b = b * a;
  let a = a - 1;
  cps if a == 1
    then return b
    else identity;
}

while syntax (std::procedural::statement)
This macro may only be used as a statement within do{} blocks, and accepts a boolean expression and the same loop variables and body as loop_over. While the former only keeps track of the loop variables within the loop body, here all loop variables keep their value for the rest of the block.

const factorial := \i. do {
  while i > 0 (i, prod=1) {
    let prod = prod * i;
    let i = i - 1;
  };
  prod
}

Note that bindings within the loop that aren't loop variables are not visible outside the loop, that while must be followed by a semicolon like other Orchid statements but unlike while loops in most procedural languages, and that the condition can reference loop variables initialized in the loop variable list despite appearing before them in the code.

recursive syntax
This macro is a relatively low-level wrapper for the Y-combinator which avoids some common pitfalls around manually emulating loop variables. It takes a name that the recursive callback will be bound to (convenitonally set to r if available), a variable list like the other loops, and captures all code up to the next closing paren as a body. Unlike the other loops, this one doesn't imply a do{} body and doesn't automatically forward values wherever r appears, instead, it's the user's responsibility to pass the correct number of arguments to r, one for each variable.

-- excerpt from std::list
export const map := \list. \f. (
  recursive r (list)
    pop list end \head. \tail.
      cons (f head) (r tail)
)

info

Passing a different number of arguments to r than the number of loop variables generates either a function or a function call with a number of arguments specified at runtime, which is an unusual but legitimate use of the macro. A recursive expression which contains both a call with an extra argument and a call with too few arguments is computationally equivalent to a stack machine.

Y (T -> T) -> T
This constant is the basis for all other recursive structures, the Y-combinator. It takes a function f of type T -> T and returns T by calling f (Y f). In an eager language this would be an infinite loop, but since Orchid is lazy it is completely safe so long as f defers the evaluation of its argument.

Map

A map implemented as a list of key-value pairs. This isn't very efficient so it should only be used with relatively few keys.

These maps may be well-formed if each key appears exactly once, or degenerate if a key appears multiple times. Which one to use is up to the programmer. Well-formed maps are managed with set and del, while degenerate maps are managed with add and delall. A degenerate map may be turned into a well-formed map via normalize which is O(n**2).

empty map ? ?
Creates a new empty map

add map K V -> K -> V -> map K V
Adds the element to the map, possibly shadowing an existing element with the same value

get map K V -> K -> option V
Returns the first value associated with this key if it exists

del map K V -> K -> map K V
Removes the first occurrence of a key.

delall map K V -> K -> map K V
Removes all occurrences of a key.

set map K V -> K -> V -> map K V
Replace at most one occurrence of a key with a new one.

normalize map K V -> map K V
ensure that there's only one instance of each key in the map.

new[] expression
Creates a new list from a comma-separated sequence of key = value pairs.

const foo := map::new[ "foo" = 1, "bar" = 2, "baz" = 3, "bar" = 4 ];

Option

option T = U -> (T -> U) -> U

An option either contains a value or contains nothing. It is represented as a function which takes two parameters. If the option is empty, it returns the first. If the option contains a value, the second parameter is applied as a function to the contained value.

some T -> option T
Wraps the value in an option

none option ?
The empty option.

map option T -> (T -> U) -> option U
Applies a function to the value in the option.

flatten option (option T) -> option T

flatmap option T -> (T -> option U) -> U

unwrap option T -> T
returns the value in the option, or panics if none is found

Procedural

This library contains syntax constructs to break code into multiple statements and name bindings which can be read in a familiar top-down order.

do{} expression
a modular block that converts semicolon-delimited statements into a linked list of statement tokens, enabling both the standard library and external modules to define custom statements.

import std::to_usize

const main := do {
  let a = 5;
  cps b = readln;
  let c = to_usize b;
  c + a
}

statement expression(do)
It's possible to add support for custom statements by defining macros with a pattern of statement ( <custom statement matcher> ) (...$next) where next is an expression describing the rest of the block. This mechanism is used to define std::loop::while for example.

let syntax(statement)
let statements bind an expression to a name so it can be used in subsequent lines.

do {
  let a = 5 + 2;
  let b = (\x. x * x);
  b a
}

cps syntax(statement)
a statement to encapsulate a callback flow, passing control to another function. Most commonly used to allow the called function to execute impure operations.

const load_data := \callback. callback 10 20

const main := do {
  cps a, b = load_data;
  a + b
}

do cps {} expression
Allows to return a sequence of statements as a cps operation which can be called in other blocks. Most commonly used to put sequences of cps operations in the branches of a conditional.

String processing

In Rust the string type these functions operate on is called OrcString. This is either a token for an interned string or an Rc<String> if the string was been programmatically constructed (eg. using the functions in this library).

All functions here operate on Unicode graphemes. This essentially means that letters with added diacritics and Mandarin multi-codepoint characters are treated as a single character. It also means that there isn't a "character" type, because a single character of zalgo text may be very large.

size OrcString -> usize
Finds the size of the text in bytes. This should be used to determine the computational resource utilization of strings. It should not be used to decide whether to truncate text.

len OrcString -> usize
Finds the number of graphemes in the text. This can be used for example to truncate text. It should not be used to limit the size of messages for security purposes.

concat OrcString -> OrcString -> OrcString
Appends a string to another. Also available as infix operator ++ which is available globally.

slice OrcString -> usize -> usize -> OrcString
Takes a string, a start and a length in graphemes. Slices out the specified subsection of the string.

find OrcString -> OrcString -> Option usize
If the first string contains the second then returns the index. See std::option for how to operate on the value.

split OrcString -> usize -> tuple[OrcString, OrcString]
Splits the string into two substrings at the nth grapheme. See std::tuple for how to operate on the value

char_atOrcString -> usize -> OrcString
Returns the nth grapheme.

Tuple

Tuples are a primitive datatype. Together with std::option, it can build any datastructure.

Tuples are constructed with a macro and consumed by a single function which reads out a field. To read data from a tuple you need to know its full length.

t expression List constructor for a tuple

const numbers := t[1, 2, 3]
const main := pick numbers 1 3
-- evaluates to 2

pick tuple -> number -> number -> ? Given a 0-based index and the total number of fields in the tuple, returns the value of the requested field.

STD

These are constants defined directly under std, typically due to their special significance or fundamental nature.

inspect T -> T
Debug-logs its argument then returns it without normalization.

panic OrcString -> nothing
Prints an error with a customizable error message, then halts. This expression never returns, so it can be used to mark dead code paths.

known::, token
The sole purpose of this export is to ensure that this token has the same namespaced name across files in usercode. , is an intrinsic operator in Orchid but without an import it would just be a local name in each module, however needing to import it would lead to confusion as it's also used in the import statement.

conv

Functions for converting types. All of these act as identity when passed their output type.

to_float usize|float|OrcString -> float
Converts a usize or OrcString to a float.

to_uint usize|float|OrcString -> usize
Converts a float or OrcString to a usize.

to_string usize|float|bool|OrcString -> OrcString
Converts a float, usize or bool to an OrcString

System::Async

Asynchrony in Orchid is realised via a central event loop that is available for all systems. As such, the tools used to control asynchrony are universal.

yield cmd
A standalone command. When returned as the value of a program, instructs the event queue to wait for the next event.

export const print := \text. \ok. (
  io::write_str io::stdout text
    (io::flush io::stdout
      ok
      (\e. panic "println threw on flush")
      \_. yield
    )
    (\e. panic "print threw on write")
    \_. yield
)

Orchid's IO is actually completely asynchronous; write_str and flush both take three callbacks, success, failure, and the sync next step. Since print blocks until the text is on the screen, after each of these commands we place the continuation in success and yield as the sync next step. If we wanted to do more work while our text gets printed, we could've used write_str directly and put the rest of the operation in its sync next step.

set_timer bool -> number -> cmd -> (cmd -> cmd) -> cmd
Sets a timer. Parameters specify whether the timer is recurring, the delay in seconds, the command to be executed when the timer fires, and the sync next step. The sync next step receives a canceller which is itself a command. If this command is called, later iterations of the timer will not run.

import system::async::(set_timer, yield)
import system::io::(readln, println)
import std::exit_status

const main := do{
  cps cancel = set_timer true 1 (println "y" yield);
  cps _ = readln;
  cps cancel;
  exit_status::success
}

System::IO

IO primitives to work with streams. Streams can be defined by the environment or returned by external libraries.

The following definitions mention types ReadHandle, WriteHandle and Error. ReadHandle and WriteHandle are created externally. Error is meant to be an Orchid representation of Rust's std::io::Error, but for the time being it's just the integer 0.

print OrcString -> cmd -> cmd
Writes the string to stdout, flushes, and continues once both operations completed.

println OrcString -> cmd -> cmd
Writes the string with a newline to stdout and flushes.

readln (OrcString -> cmd) -> cmd
Reads a line from stdin.

Standard streams

Standard unix I/O streams

stdin ReadHandle
stdout WriteHandle
stderr WriteHandle

I/O operations

An IO operation IoOperation T has the type T -> (Error -> cmd) -> cmd -> cmd. It is a command that takes an eventual success and error handler, and a continuation. The continuation does not wait for the current operation to complete. Operations on the same ReadHandle or WriteHandle are sequenced, but there are no ordering guarantees, so eg. if you want to write to a WriteHandle and then flush it, the flush call must appear in the success path and not the continuation.

read_string ReadHandle -> IoOperation (OrcString -> cmd)
Reads the entire stream to a string.

read_line ReadHandle -> IoOperation (OrcString -> cmd)
Reads the stream up to the next line break, consuming the line break and returning all text before it.

read_bin ReadHandle -> IoOperation (Binary -> cmd)
Reads the entire stream into a blob as defined in std::binary.

read_n_bytes ReadHandle -> usize -> IoOperation (Binary -> cmd)
Reads N bytes from the stream and returns them in a blob. Remember that bytes don't translate to Unicode characters.

read_until ReadHandle -> usize -> IoOperation (Binary -> cmd)
Accepts a byte value no greater than 255. Reads the stream until the specified byte is found.

write_str WriteHandle -> OrcString -> IoOperation cmd
Writes the given string to the stream.

write_bin WriteHandle -> Binary -> IoOperation cmd
Writes the given bytes to the stream.

flush WriteHandle -> IoOperation cmd
Signals for any buffered data in the stream to be flushed. If you're writing to standard output explicitly instead of using print, you need to call this manually.