gyre.nodes

Structure and interpretation of Gyre nodes.

This module implements Gyre nodes (and edges). Each structure's documentation also explains its intended semantics, albeit this may be mixed with implementation details.

Members

Enums

EdgeKind
enum EdgeKind

Possible edge kinds, or "colors".

Structs

AdditionNoOverflowSignedNode
struct AdditionNoOverflowSignedNode

Two's complement no-overflow signed addition.

AdditionNode
struct AdditionNode

Two's complement addition operation (works for both signed and unsigned integers).

AndNode
struct AndNode

Bitwise AND operation.

ConditionalNode
struct ConditionalNode

Directs control flow to exactly one of multiple possible edges.

ConstantNode
struct ConstantNode

Constructs a constant value of a certain type.

EqualNode
struct EqualNode

Compares two bit patterns for equality.

ForkNode
struct ForkNode

Forks a single control flow into multiple concurrent ones.

InEdge
struct InEdge

An in-edge slot.

InEdgeIterator
struct InEdgeIterator(Callable)

Iterates (with foreach) over a node's in-edges.

InstantiationNode
struct InstantiationNode

Instantiates a join node.

JoinNode
struct JoinNode

Gyre's main mechanism for procedural abstraction, the join node.

JumpNode
struct JumpNode

Yields control flow to another part of the program through a "jump with arguments".

LeftShiftNoOverflowNode
struct LeftShiftNoOverflowNode

Bitwise left-shift with no-overflow semantics; shifts in zeros.

LeftShiftNode
struct LeftShiftNode

Bitwise left-shift operation; shifts in zeros.

MacroNode
struct MacroNode

Gyre's mechanism for structural abstraction, the macro node.

MultiplexerNode
struct MultiplexerNode

An operation which chooses one of its inputs to forward as a result.

MultiplicationNoOverflowSignedNode
struct MultiplicationNoOverflowSignedNode

Two's complement no-overflow signed multiplication.

MultiplicationNode
struct MultiplicationNode

Two's complement multiplication operation.

Node
struct Node

Common prefix shared by all nodes, safely used ONLY through pointers.

NotEqualNode
struct NotEqualNode

Compares two bit patterns for inequality.

OrNode
struct OrNode

Bitwise OR operation.

OutEdge
struct OutEdge

An outgoing edge.

OutEdgeIterator
struct OutEdgeIterator(Callable)

Iterates (with foreach) over a node's out-edges.

SignedDivisionNode
struct SignedDivisionNode

Two's complement division operation for signed operands, rounds towards zero.

SignedExtensionNode
struct SignedExtensionNode

Yields a wider version of its input, with added bits equal to the input's sign bit.

SignedGreaterOrEqualNode
struct SignedGreaterOrEqualNode

Computes whether a (signed) two's complement integer is greater than or equal to another.

SignedLessThanNode
struct SignedLessThanNode

Computes whether a (signed) two's complement integer is strictly less than another.

SignedRemainderNode
struct SignedRemainderNode

Two's complement remainder operation for signed operands, rounds towards zero.

SignedRightShiftNode
struct SignedRightShiftNode

Arithmetic right-shift operation; bits shifted in are equal to the input's most significant bit.

SubtractionNoOverflowSignedNode
struct SubtractionNoOverflowSignedNode

Two's complement no-overflow signed subtraction.

SubtractionNode
struct SubtractionNode

Two's complement subtraction operation (works for both signed and unsigned integers).

TruncationNode
struct TruncationNode

Yields the lowermost bits of its input.

UndefinedNode
struct UndefinedNode

Constructs a "don't care" value of a certain type.

UnsignedDivisionNode
struct UnsignedDivisionNode

Two's complement division operation for unsigned operands, rounds towards zero.

UnsignedExtensionNode
struct UnsignedExtensionNode

Yields a wider version of its input, with added bits set to zero.

UnsignedGreaterOrEqualNode
struct UnsignedGreaterOrEqualNode

Computes whether a (unsigned) two's complement integer is greater than or equal to another.

UnsignedLessThanNode
struct UnsignedLessThanNode

Computes whether a (unsigned) two's complement integer is strictly less than another.

UnsignedRemainderNode
struct UnsignedRemainderNode

Two's complement remainder operation for unsigned operands, rounds towards zero.

UnsignedRightShiftNode
struct UnsignedRightShiftNode

Logical right-shift operation; shifts in zeros.

XorNode
struct XorNode

Bitwise XOR operation.

mnemonic
struct mnemonic

See gyre.mnemonics.

Detailed Description

Poison values and UB

In Gyre, every operation may have some conditions imposed on its inputs in order to produce correct behavior / a sane value. When the result of a data-only operation isn't well-defined (e.g. integer division by zero), it produces a "poison". Poison values, as in LLVM, indicate invalid program behavior; it is as if every instance of a poison value came from the result of 0/0. Furthermore, these values are "poisonous" because any operation with a result which depends on a poison operand will also produce poison. Note that in some cases a result doesn't actually depend on the value of (all of) its operands (e.g. x * 0 is always 0).

This is not unlike C's infamous "Undefined Behavior", because a Gyre compiler may (while respecting program semantics) assume that poison values are never used, which in turn may help with some optimizations (e.g. loop-invariant code motion). Another option is to issue warnings or errors when such erroneous behavior is detected. In this specific implementation, we don't (by default) do aggressive optimizations based on U.B. / poison usage.

Prim ops rationale

It's hard to justify our choice of primitive operations when we know binary NAND would have sufficed to express most of them. We're essentially copying existing IRs (LLVM, MLIR, Thorin, SPIR-V, WASM, etc), which raise the minimum abstraction level to two's complement integer arithmetic. Reasoning at that level makes it easier to perform trivial transformations and optimizations (which would require deeper pattern matching if using NANDs only). Then, due to C's status as a de facto lingua franca of programming languages, having our primitive operations match that lowest common denominator will probably benefit compiler performance in most cases, which wouldn't be as true if our primitives were completely different than C's.

Overflow semantics

Some of the last primitives to be added were the "no-overflow" variants of some operations. They could have been expressed with macro nodes, and having them as primitives means that we're introducing more situations in which the same value is being produced at different nodes. However, the signed versions are very frequent operations in C-derived languages and we wouldn't want the compiler to lose performance whenever they're used. And they exist for good reason, as they really do help the compiler perform more arithmetic simplifications, especiallly when combining different operations in the same expression (e.g. (x * 30) / 15 can only be transformed into x * 2 if overflow is not a possibility). In short: even if the operation being performed is the same, the no-overflow versions carry more information about their operands.

To be clear: a no-overflow node performs the same operation as its wrap-on-overflow version, but there's an implicit contract put on its arguments. This contract states that the operation could have been computed over wider integer types, but the result would still be the same. As an example, take the expression (x << 1) >> 1. If we assume that << can overflow (i.e. x has a non-zero MSB which is being thrown away), this is equivalent to x & 0b011...11 (i.e. set the MSB to zero); by assuming that overflow is not possible, the expression can be reduced to just x (because if we had used a wider integer, that would have been the result).