Possible edge kinds, or "colors".
Two's complement no-overflow signed addition.
Two's complement addition operation (works for both signed and unsigned integers).
Bitwise AND operation.
Directs control flow to exactly one of multiple possible edges.
Constructs a constant value of a certain type.
Compares two bit patterns for equality.
Forks a single control flow into multiple concurrent ones.
An in-edge slot.
Iterates (with foreach) over a node's in-edges.
Instantiates a join node.
Gyre's main mechanism for procedural abstraction, the join node.
Yields control flow to another part of the program through a "jump with arguments".
Bitwise left-shift with no-overflow semantics; shifts in zeros.
Bitwise left-shift operation; shifts in zeros.
Gyre's mechanism for structural abstraction, the macro node.
An operation which chooses one of its inputs to forward as a result.
Two's complement no-overflow signed multiplication.
Two's complement multiplication operation.
Common prefix shared by all nodes, safely used ONLY through pointers.
Compares two bit patterns for inequality.
Bitwise OR operation.
An outgoing edge.
Iterates (with foreach) over a node's out-edges.
Two's complement division operation for signed operands, rounds towards zero.
Yields a wider version of its input, with added bits equal to the input's sign bit.
Computes whether a (signed) two's complement integer is greater than or equal to another.
Computes whether a (signed) two's complement integer is strictly less than another.
Two's complement remainder operation for signed operands, rounds towards zero.
Arithmetic right-shift operation; bits shifted in are equal to the input's most significant bit.
Two's complement no-overflow signed subtraction.
Two's complement subtraction operation (works for both signed and unsigned integers).
Yields the lowermost bits of its input.
Constructs a "don't care" value of a certain type.
Two's complement division operation for unsigned operands, rounds towards zero.
Yields a wider version of its input, with added bits set to zero.
Computes whether a (unsigned) two's complement integer is greater than or equal to another.
Computes whether a (unsigned) two's complement integer is strictly less than another.
Two's complement remainder operation for unsigned operands, rounds towards zero.
Logical right-shift operation; shifts in zeros.
Bitwise XOR operation.
See gyre.mnemonics.
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.
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.
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).
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.