Layer 2: Instructions
This chapter looks at the lowest level of the WIR, which implements a simply assembly-like instruction language for manipulating the stack that determines the graph control flow.
The edges of the graph themselves are defined in the previous chapter, and the chapter before that talks about the toplevel of the WIR representation tying the edges together.
We use the same conventions as in the previous chapters. We recommend you read them before continuing to understand what we wrote down.
EdgeInstrs
A Linear-edge may be annotated with zero or more EdgeInstructions, which are assembly-like instructions that manipulate a workflow-wide stack. Some Edges manipulate the stack too, most often reading from it, but most work is done by explicitly stating the instructions.
As discussed in the introduction chapter, the instructions essentially implement a second layer of computation. Where the edges mostly related to "on-graph" computation, the edge instructions can be thought of as "off-graph" computation that typically matters less for reasoners.
As far as the specification goes, the the following fields are shared by all instructions:
kind(string): Denotes which variant of theEdgeInstrthis object describes. The identifiers used are given below in each subsections.
A convenient index of all instructions:
Cast: Casts a value to another type.Pop: Discards the top value on the stack.PopMarker: Pushes an invisible value that is used by...DynamicPop: Pops values off the stack until the invisible value pushed byPopMarkeris popped.Branch: Conditionally jumps to another instruction within the same stream.BranchNot: Conditionally jumps to another instruction within the same stream but negated.Not: Performs logical negation.Neg: Performs arithmetic negation.And: Performs logical conjunction.Or: Performs logical disjunction.Add: Performs arithmetic addition / string concatenation.Sub: Performs arithmetic subtraction.Mul: Performs arithmetic multiplication.Div: Performs arithmetic division.Mod: Performs arithmetic modulo.Eq: Compares two values.Ne: Compares two values but negated.Lt: Compares two numerical values in a less-than fashion.Le: Compares two numerical values in a less-than-or-equal-to fashion.Gt: Compares two numerical values in a greater-than fashion.Ge: Compares two numerical values in a greater-than-or-equal-to fashion.Array: Pushes an array literal onto the stack (or rather, creates one out of existing values).ArrayIndex: Takes an array and an index and pushes the element of the array at that index.Instance: Pushes a new instance of a class onto the stack by popping existing values.Proj: Projects a field on an instance to get that field's value.VarDec: Declares the existance of a variable.VarUndec: Releases the resources of a variable.VarGet: Gets the value of a variable.VarSet: Sets the value of a variable.Boolean: Pushes a boolean constant.Integer: Pushes a integer constant.Real: Pushes a floating-point constant.String: Pushes a string constant.Function: Pushes a function handle on top of the stack.
Cast
Identifier: "cst"
The Cast takes the top value off the stack and attempts to perform a type conversion on it. Then, the result is pushed back on top of the stack.
Specification-wise, the Cast needs one additional field:
t(DataType): Defines the datatype the top value on the stack to.
Stack-wise, the Cast manipulates the stack in the following way:
- pops
DataType::Anyfrom the stack; and - pushes An object of type
ton top of the stack representing the same value but as another type.
Note that this conversion may fail, since not all types can be converted into other types. Specifically, the following convertions are defined, where T and U represent arbitrary types and Ts and Us represents lists of arbitrary, possibly heterogenously typed values:
booltointcreates a 1 if the input value is true or a 0 otherwise.booltostrcreates a "true" if the input value is true, or a "false" if the input value is false.inttoboolcreates true if the input value is non-zero, or false otherwise.inttorealcreates an equivalent floating-point number.inttostrwrites the integer as a serialized number.realtointwrites the floating-point value rounded down to the nearest integer.realtostrwrites the floating-point number as a serialized number.arr<T>tostrcasts the elements in the array fromTtostrand then serializes them within square brackets ([]), separated by comma's (,) (e.g.,"[ 42, 43, 44 ]").arr<T>toarr<U>casts every element in the array fromTtoU.func(Ts) -> Ttofunc(Us) -> Uis a no-op but only ifTs == UsandT == U.func(Ts) -> Ttostrcasts the values inTstostrandTtostrand then writes the name of the function followed by the arguments in parenthesis (()) followed by->and the return type (e.g.,"foo(int, real) -> str"). If the function is a class method, then the class name and::are prefixed to the function name (e.g.,Foo::foo(Foo, int, real) -> str).func(Ts) -> Ttoclssis a no-op, even keeping the same type, but only if the function is a method and belongs to the class it is casted to. This can be used to assert that a method is part of a class(?).clss<Ts>tostrcasts the values inTstostrand then serializes it as the class name followed by the values in the class serialized as name:=value separated by comma's in between curly brackets ({}) (e.g.,Foo { foo := 42, bar := "test" }).DatatostrwritesData, and then the name of the data in triangular brackets (<>) (e.g.,"Data<Foo>").DatatoIntermediateResultis a no-op.IntermediateResulttostrwritesIntermediateResult, and then the name of the data in triangular brackets (<>) (e.g.,"IntermediateResult<Foo>").TtoTperforms a no-op.TtoAnyperforms a no-op and just changes the type.
Any convertion not mentioned here is defined to be illegal.
As such, the Cast can throw the following errors:
Empty stackwhen popping;Stack overflowwhen pushing; orIllegal castwhen the value cannot be casted to typet.
The following is an example Cast-instruction:
{
"kind": "cst",
"t": "str"
}
Pop
Identifier: "pop"
Pops the top value off the stack and discards it.
This instruction does not require any additional fields.
Stack-wise, the Pop does the following:
- pops
DataType::Anyfrom the stack.
It may throw the following error:
Empty stackwhen popping.
An example:
{
"kind": "pop"
}
PopMarker
Identifier: "mpp"
Pushes a so-called pop marker onto the stack, which can then be popped using the DynamicPop-instruction. This combination can be used when popping an unknown number of values off the stack.
This instruction does not require any additional fields.
Stack-wise, the PopMarker does the following:
- pushes a special value onto the stack that is invisible to most operations except
DynamicPop.
It may throw the following error:
Stack overflowwhen pushing.
An example:
{
"kind": "mpp"
}
DynamicPop
Identifier: "dpp"
Dynamically pops the stack until a PopMarker is popped. This combination can be used when popping an unknown number of values off the stack.
This instruction does not require any additional fields.
Stack-wise, the PopMarker does the following:
- pops a dynamic amount of values off the stack until
PopMarker's special value is popped.
Doing so may make it throw the following error:
Empty stackwhen popping.
An example:
{
"kind": "dpp"
}
Branch
Identifier: "brc"
Not to be confused with the Branch-edge, this instruction implements a branch in the instruction stream only. This is only allowed when it's possible to do this within the same linear edge, implying the branch does not influence directly which nodes are executed.
The branch is taken when the top value on the stack is true; otherwise, it is ignored and implements a no-op.
The Branch defines the following fields:
n(number): The relative offset to jump to. This can be thought of as "number of instructions to skip", where a value of -1 points to the previous instruction, 0 points to theBranch-instruction itself and 1 points to the next instruction.
Stack-wise, the Branch does the following:
- pops
DataType::Booleanfrom the stack.
As such, it can throw the following errors:
Empty stackwhen popping; orType errorwhen the popped value is not abool.
Note that skipping outside of the sequence of instructions belonging to a Linear-edge means the VM simply stops executing.
An example Branch-instruction:
{
"kind": "brc",
"n": 5
}
BranchNot
Identifier: "brn"
Counterpart to the Branch-instruction that behaves the same except that it branches when the value on the stack is false instead of true.
The BranchNot defines the following fields:
n(number): The relative offset to jump to. This can be thought of as "number of instructions to skip", where a value of -1 points to the previous instruction, 0 points to theBranchNot-instruction itself and 1 points to the next instruction.
Stack-wise, the Branch does the following:
- pops
DataType::Booleanfrom the stack.
As such, it can throw the following errors:
Empty stackwhen popping; orType errorwhen the popped value is not abool.
Note that skipping outside of the sequence of instructions belonging to a Linear-edge means the VM simply stops executing.
An example Branch-instruction:
{
"kind": "brn",
"n": 5
}
Not
Identifier: "not"
Implements a logical negation on the top value on the stack.
The Not does not need additional fields to do this.
Stack-wise, the Not does the following:
- pops
DataType::Booleanfrom the stack; and - pushes
DataType::Booleanon top of the stack.
As such, it can throw the following errors:
Empty stackwhen popping;Type errorwhen the popped value is not abool; orStack overflowwhen pushing.
Example:
{
"kind": "not"
}
Neg
Identifier: "neg"
Implements a arithmetic negation on the top value on the stack.
The Neg does not need additional fields to do this.
Stack-wise, the Neg does the following:
- pops
DataType::Numericfrom the stack; and - pushes
DataType::Numericon top of the stack.
As such, it can throw the following errors:
Empty stackwhen popping;Type errorwhen the popped value is not anintorreal; orStack overflowwhen pushing.
Example:
{
"kind": "neg"
}
And
Identifier: "and"
Performs logical conjunction on the top two values on the stack.
No additional fields are needed to do this.
Stack-wise, the And does the following:
- pops
DataType::Booleanfor the righthand-side; - pops
DataType::Booleanfor the lefthand-side; and - pushes
DataType::Booleanthat is the conjunction of the LHS and RHS.
The following errors can occur during this process:
Empty stackwhen popping;Type errorwhen the popped values are not abool; orStack overflowwhen pushing.
Example:
{
"kind": "and"
}
Or
Identifier: "or"
Performs logical disjunction on the top two values on the stack.
No additional fields are needed to do this.
Stack-wise, the Or does the following:
- pops
DataType::Booleanfor the righthand-side; - pops
DataType::Booleanfor the lefthand-side; and - pushes
DataType::Booleanthat is the disjunction of the LHS and RHS.
The following errors can occur during this process:
Empty stackwhen popping;Type errorwhen the popped values are not abool; orStack overflowwhen pushing.
Example:
{
"kind": "or"
}
Add
Identifier: "add"
Performs arithmetic addition or string concatenation on the top two values on the stack. Which of the two depends on the types of the popped values.
The Add does not introduce additional fields.
Stack-wise, the Add does the following:
- pops
DataType::Integer,DataType::RealorDataType::Stringfor the righthand-side; - pops
DataType::Integer,DataType::RealorDataType::Stringfor the lefthand-side; and - pushes
DataType::Integer,DataType::RealorDataType::Stringdepending on the input types:- If both arguments are
DataType::Integer, then a new integer is pushed that is the arithmetic addition of the LHS and RHS; - If both arguments are
DataType::Real, then a new real is pushed that is the arithmetic addition of both the LHS and RHS; and - If both arguments are
DataType::String, then a new string is pushed that is the concatenation of the LHS and then the RHS.
- If both arguments are
The following errors may occur when processing an Add:
Empty stackwhen popping;Type errorwhen the popped values do not match any of the three cases above;Overflow errorwhen the addition results in integer/real addition; orStack overflowwhen pushing.
Example:
{
"kind": "add"
}
Sub
Identifier: "sub"
Performs arithmetic subtraction on the top two values on the stack.
The Sub does not introduce additional fields.
Stack-wise, the Sub does the following:
- pops
DataType::IntegerorDataType::Realfor the righthand-side; - pops
DataType::IntegerorDataType::Realfor the lefthand-side; and - pushes
DataType::IntegerorDataType::Realdepending on the input types:- If both arguments are
DataType::Integer, then a new integer is pushed that is the arithmetic subtraction of the LHS and RHS; and - If both arguments are
DataType::Real, then a new real is pushed that is the arithmetic subtraction of both the LHS and RHS.
- If both arguments are
The following errors may occur when processing an Add:
Empty stackwhen popping;Type errorwhen the popped values do not match any of the two cases above;Overflow errorwhen the subtraction results in integer/real underflow; orStack overflowwhen pushing.
Example:
{
"kind": "sub"
}
Mul
Identifier: "mul"
Performs arithmetic multiplication on the top two values on the stack.
The Mul does not introduce additional fields.
Stack-wise, the Mul does the following:
- pops
DataType::IntegerorDataType::Realfor the righthand-side; - pops
DataType::IntegerorDataType::Realfor the lefthand-side; and - pushes
DataType::IntegerorDataType::Realdepending on the input types:- If both arguments are
DataType::Integer, then a new integer is pushed that is the arithmetic multiplication of the LHS and RHS; and - If both arguments are
DataType::Real, then a new real is pushed that is the arithmetic multiplication of both the LHS and RHS.
- If both arguments are
The following errors may occur when processing an Add:
Empty stackwhen popping;Type errorwhen the popped values do not match any of the two cases above;Overflow errorwhen the multiplication results in integer/real overflow; orStack overflowwhen pushing.
Example:
{
"kind": "mul"
}
Div
Identifier: "div"
Performs arithmetic division on the top two values on the stack.
The Div does not introduce additional fields.
Stack-wise, the Div does the following:
- pops
DataType::IntegerorDataType::Realfor the righthand-side; - pops
DataType::IntegerorDataType::Realfor the lefthand-side; and - pushes
DataType::IntegerorDataType::Realdepending on the input types:- If both arguments are
DataType::Integer, then a new integer is pushed that is the integer division of the LHS and RHS (i.e., rounded down to the nearest integer); and - If both arguments are
DataType::Real, then a new real is pushed that is the floating-point division of both the LHS and RHS.
- If both arguments are
The following errors may occur when processing an Add:
Empty stackwhen popping;Type errorwhen the popped values do not match any of the two cases above;Overflow errorwhen the division results in real underflow; orStack overflowwhen pushing.
Example:
{
"kind": "div"
}
Mod
Identifier: "mod"
Computes the remainder of dividing one value on top of the stack with another.
The Mod does not introduce additional fields to do so.
Stack-wise, the Mod does the following:
- pops
DataType::Integerfor the righthand-side; - pops
DataType::Integerfor the lefthand-side; and - pushes
DataType::Integerthat is the remainder of dividing the LHS by the RHS.
The following errors may occur when processing a Mod:
Empty stackwhen popping;Type errorwhen the popped values are notints; orStack overflowwhen pushing.
Example:
{
"kind": "mod"
}
Eq
Identifier: "eq"
Compares the top two values on the stack for equality. This is first type-wise (their types must be equal), and then value-wise.
No additional fields are introduced to do so.
Stack-wise, the Eq does the following:
- pops
DataType::Anyfor the righthand-side; - pops
DataType::Anyfor the lefthand-side; and - pushes
DataType::Booleanwith true if the LHS equals the RHS, or false otherwise.
The following errors may occur when processing an Eq:
Empty stackwhen popping; orStack overflowwhen pushing.
Example:
{
"kind": "eq"
}
Ne
Identifier: "ne"
Compares the top two values on the stack for inequality. This is first type-wise (their types must be unequal), and then value-wise.
No additional fields are introduced to do so.
Stack-wise, the Ne does the following:
- pops
DataType::Anyfor the righthand-side; - pops
DataType::Anyfor the lefthand-side; and - pushes
DataType::Booleanwith true if the LHS does not equal the RHS, or false otherwise.
The following errors may occur when processing an Eq:
Empty stackwhen popping; orStack overflowwhen pushing.
Example:
{
"kind": "ne"
}
Lt
Identifier: "lt"
Compares the top two values on the stack for order, specifically less-than. This can only be done for numerical values.
No additional fields are introduced to do so.
Stack-wise, the Lt does the following:
- pops
DataType::Numericfor the righthand-side; - pops
DataType::Numericfor the lefthand-side; and - pushes
DataType::Booleanwith true if the LHS is stricly less than the RHS, or false otherwise.
The following errors may occur when processing an Lt:
Empty stackwhen popping;Type errorwhen either argument is not anumor they are not of the same type (i.e., cannot compareintwithreal); orStack overflowwhen pushing.
Example:
{
"kind": "lt"
}
Le
Identifier: "le"
Compares the top two values on the stack for order, specifically less-than-or-equal-to. This can only be done for numerical values.
No additional fields are introduced to do so.
Stack-wise, the Le does the following:
- pops
DataType::Numericfor the righthand-side; - pops
DataType::Numericfor the lefthand-side; and - pushes
DataType::Booleanwith true if the LHS is less than or equal to the RHS, or false otherwise.
The following errors may occur when processing an Le:
Empty stackwhen popping;Type errorwhen either argument is not anumor they are not of the same type (i.e., cannot compareintwithreal); orStack overflowwhen pushing.
Example:
{
"kind": "le"
}
Gt
Identifier: "gt"
Compares the top two values on the stack for order, specifically greater-than. This can only be done for numerical values.
No additional fields are introduced to do so.
Stack-wise, the Gt does the following:
- pops
DataType::Numericfor the righthand-side; - pops
DataType::Numericfor the lefthand-side; and - pushes
DataType::Booleanwith true if the LHS is strictly greater than to the RHS, or false otherwise.
The following errors may occur when processing an Gt:
Empty stackwhen popping;Type errorwhen either argument is not anumor they are not of the same type (i.e., cannot compareintwithreal); orStack overflowwhen pushing.
Example:
{
"kind": "gt"
}
Ge
Identifier: "ge"
Compares the top two values on the stack for order, specifically greater-than-or-equal-to. This can only be done for numerical values.
No additional fields are introduced to do so.
Stack-wise, the Ge does the following:
- pops
DataType::Numericfor the righthand-side; - pops
DataType::Numericfor the lefthand-side; and - pushes
DataType::Booleanwith true if the LHS is greater than or equal to the RHS, or false otherwise.
The following errors may occur when processing an Ge:
Empty stackwhen popping;Type errorwhen either argument is not anumor they are not of the same type (i.e., cannot compareintwithreal); orStack overflowwhen pushing.
Example:
{
"kind": "ge"
}
Array
Identifier: "arr"
Consumes a number of values from the top off the stack and creates a new Array with them.
The Array specifies the following additional fields:
l(number): The number of elements to pop, i.e., the length of the array.t(DataType): The data type of the array. Note that this includes thearrdatatype and its element type.
Stack-wise, the Array does the following:
- pops
lvalues of typet; and - pushes
DataType::Arraywithlelements of typet. Note that the order of elements is reversed (i.e., the first element popped is the last element of the array).
Doing so may trigger the following errors:
Empty stackwhen popping;Type errorwhen a popped value does not have typet; orStack overflowwhen pushing.
Example:
// Represents an array literal of length 5, element type `str`
{
"kind": "arr",
"l": 5,
"t": { "kind": "arr", "t": { "kind": "str" } }
}
ArrayIndex
Identifier: "arx"
Indexes an array value on top of the stack with some index and outputs the element at that index.
The ArrayIndex specifies the following additional fields:
t(DataType): The data type of the element. This is the type of the pushed value.
Stack-wise, the ArrayIndex does the following:
- pops
DataType::Integerfor the index; - pops
DataType::Arrayfor the array to index, which must have element typet; and - pushes a value of type
tthat is the element at the specified index.
Doing so may trigger the following errors:
Empty stackwhen popping;Type errorwhen any of the popped values are incorrectly typed;Array out-of-boundswhen the index is too large for the given array, or if it's negative; orStack overflowwhen pushing.
Example:
/// Indexes an array of string elements
{
"kind": "arx",
"t": { "kind": "str" }
}
Instance
Identifier: "ins"
Consumes a number of values on top of the stack in order to construct a new instance of a class.
To do so, the Instance defines additional fields:
d(number): Identifier of the type which we are constructing. This definition is given in the parent symbol table.
Stack-wise, the Instance does the following:
- pops
Nvalues of varying types from the top off the stack, whereNis the number of fields (which may be 0) and the types are those matching to the fields. Note that they are popped in reverse alphabetical order, e.g., fieldais below fieldzon the stack; and - pushes a value of the type referred to by
d.
Doing so may trigger the following errors:
Unknown definitionifdis not known in the symbol tables;Empty stackwhen popping;Type errorwhen any of the popped values are incorrectly typed; orStack overflowwhen pushing.
Example:
{
"kind": "ins",
"d": 42
}
Proj
Identifier: "prj"
The Proj instruction takes an instance and retrieves the value of the given field from it.
The field is embedded in the instruction. As such, it adds the following specification fields:
f(string): The name of the field to project.
Stack-wise, the Proj does the following:
- pops an instance value; and
- pushes the value of field
fin that instance.
The projection ducktypes the instance, and as such can trigger the following errors:
Empty stackwhen popping;Unknown fieldwhen the popped instance has no field by the name described inf; orStack overflowwhen pushing.
Example:
{
"kind": "prj",
"f": "foo"
}
VarDec
Identifier: "vrd"
Declares a new variable, giving an opportunity to the underlying execution context to reserve space for it.
The following fields are added by a VarDec:
d(number): The identifier of the variable definition in the parent symbol table of the variable we're declaring. Note that, because definitions are scoped to functions, this is a unique identifier for specific variable instances the current scope.
Stack-wise, the VarDec doesn't do anything, as it fully acts on the underlying variable system.
The following errors may occur when working with the VarDec:
Unknown definitionifdis not known in the symbol tables; orVariable errorif the underlying context finds another reason to crash.
Example:
{
"kind": "vrd",
"d": 42
}
VarUndec
Identifier: "vru"
Explicitly undeclares a new variable, giving an opportunity to the underlying execution context to claim back space for it.
The following fields are added by a VarUndec:
d(number): The identifier of the variable definition in the parent symbol table of the variable we're undeclaring.Note that, because definitions are scoped to functions, this is a unique identifier for specific variable instances the current scope.
Stack-wise, the VarUndec doesn't do anything, as it fully acts on the underlying variable system.
The following errors may occur when working with the VarUndec:
Unknown definitionifdis not known in the symbol tables; orVariable errorif the underlying context finds another reason to crash.
Example:
{
"kind": "vru",
"d": 42
}
VarGet
Identifier: "vrg"
Retrieves the value of a variable which was previously VarSet.
The following fields are added by a VarGet:
d(number): The identifier of the variable definition in the parent symbol table of the variable we're undeclaring.Note that, because definitions are scoped to functions, this is a unique identifier for specific variable instances the current scope.
Stack-wise, the VarGet does the following:
- pushes the value stored in the variable on top of the stack.
The following errors may occur when working with the VarGet:
Unknown definitionifdis not known in the symbol tables;Variable errorif the underlying context finds another reason to crash; orStack overflowwhen pushing.
Example:
{
"kind": "vrs",
"d": 42
}
VarSet
Identifier: "vrs"
Moves the value on top of the stack to a variable so it may be VarGet.
The following fields are added by a VarSet:
d(number): The identifier of the variable definition in the parent symbol table of the variable we're undeclaring.Note that, because definitions are scoped to functions, this is a unique identifier for specific variable instances the current scope.
Stack-wise the VarSet does the following:
- pops
DataType::Anyfrom the stack to put in the variable.
The following errors may occur when working with the VarGet:
Unknown definitionifdis not known in the symbol tables;Variable errorif the underlying context finds another reason to crash;Empty stackwhen popping; orType errorwhen the type of the popped value does not match the type of the variable.
Example:
{
"kind": "vrs",
"d": 42
}
Boolean
Identifier: "bol"
Pushes a boolean constant on top of the stack.
The following fields are added by a Boolean:
v(bool): The boolean value to push.
Stack-wise, the Boolean does the following:
- pushes
DataType::Booleanon top of the stack.
The following errors may occur when executing a Boolean:
Stack overflowwhen pushing.
Example:
{
"kind": "bol",
"v": true
}
Integer
Identifier: "int"
Pushes an integer constant on top of the stack.
The following fields are added by a Integer:
v(number): The integer value to push.
Stack-wise, the Integer does the following:
- pushes
DataType::Integeron top of the stack.
The following errors may occur when executing a Integer:
Stack overflowwhen pushing.
Example:
{
"kind": "int",
"v": -84
}
Real
Identifier: "rel"
Pushes a floating-point constant on top of the stack.
The following fields are added by a Real:
v(number): The floating-point value to push.
Stack-wise, the Real does the following:
- pushes
DataType::Realon top of the stack.
The following errors may occur when executing a Real:
Stack overflowwhen pushing.
Example:
{
"kind": "rel",
"v": 0.42
}
String
Identifier: "str"
Pushes a string constant on top of the stack.
The following fields are added by a String:
v(string): The string value to push.
Stack-wise, the String does the following:
- pushes
DataType::Stringon top of the stack.
The following errors may occur when executing a String:
Stack overflowwhen pushing.
Example:
{
"kind": "str",
"v": "Hello, world!"
}
Function
Identifier: "fnc"
Pushes a function handle on top of the stack so it may be called.
The following fields are added by a Function:
d(number): The ID of the function definition which to push on top of the stack.
Stack-wise, the Function does the following:
- pushes
DataType::Functionon top of the stack.
The following errors may occur when executing a Function:
Unknown definitionifdis not known in the symbol tables; orStack overflowwhen pushing.
Example:
{
"kind": "fnc",
"v": 42
}
Next
This chapter concludes the specification of the WIR.
If you are intending to discover the WIR bottom-up, continue to the previous chapter to see how the graph overlaying the instructions is represented.
Otherwise, you can learn other aspects of the framework or the specification by selecting a different topic in the sidebar on the left.