Intrinsics
On this page
- Declaring an intrinsic
- Why mem is a struct
- Fallback bodies
- The intrinsics
- sizeof
- alignof
- needsFinal
- finalize
- typeRecord
- holdsTraced
- trace
- traceRoots
- sliceFromParts and sliceFromPartsMut
- readRaw
- writeRaw
- moveRaw
- srcFile and srcLine
- isDebugBuild
- Constants of the build: a provisional mechanism; its final design is open
- Atomic operations
- Which types
- Orderings
- atomicLoad and atomicStore
- atomicSwap
- atomicAdd, atomicSub, atomicAnd, atomicOr and atomicXor
- atomicMin and atomicMax
- atomicCompareSwap
- What a fallback promises
- How LLVM does it
- A GPU workgroup's barriers
- An integer's bits
- countOnes, leadingZeros and trailingZeros
- rotateLeft and rotateRight
- shlMasked and shrMasked
- mulFold
- How LLVM does them
Note: Everything on this page is implemented, except that no call is yet required to be inside trust, because trust itself is not implemented. The constants of the build beside isDebugBuild, the target's OS and conec's -D, are a provisional mechanism; its final design is open.
Library code sometimes needs something only the compiler can do: say how large a type is, run a value's finalizer where it sits, or make a slice from a pointer and a count. An intrinsic is how Cone hands such a capability to library code. It looks like a function, and is called like one, but the compiler supplies what it does, expanding it where it is called.
Each intrinsic is defined by what it means in Cone, stated below with every edge case: never as "whatever the code generator's instruction does". A code generator is one way to implement that meaning. This is what lets an intrinsic mean the same thing on any back end.
Declaring an intrinsic#
An intrinsic is declared with the @intrinsic attribute after fn, and an ordinary Cone signature:
pub fn @intrinsic sizeof[T]() usize;
Only the core package may declare one: as a function of one of its modules, or as a function (taking no self) of a plain struct one of them declares. Anywhere else, @intrinsic is an error.
Core declares every intrinsic below inside mem, but for srcFile and srcLine, isDebugBuild and the provisional constants of the build, and every module, core being its prelude, reaches them through that one name:
imm p = malloc(mem.sizeof[Point]() * 4usize) as *Point; mem.writeRaw[Point](p, new Point(1., 2.));
So the intrinsics' own names are free: a module may declare its own finalize or sizeof. Only mem itself is a name every module has.
mem also holds one function that is not an intrinsic, sliceEq[T](a &Array[T], b &Array[T]) bool: the element-by-element comparison that == on two slices performs, written in Cone.
Why mem is a struct#
mem is meant to be a submodule of core, core.mem. A submodule of core cannot yet be reached from outside it: a compile that finds core by itself reads only core's one source file, and the include file other packages compile against keeps a package's submodules private. So mem is declared as an @opaque struct, which holds no value, whose functions are reached by the same path a submodule's would be. When it becomes a submodule, no call changes.
The compiler keeps a registry of the intrinsics it defines. A declaration must name one of them, and must say exactly what the registry says: the same type parameters, parameters and result. An unknown name or a different signature is an error. An intrinsic has no symbol of its own, so it may not be extern, carry @c, or have a reference to it borrowed.
A type argument must be of the kind the intrinsic accepts. Every intrinsic below needs a type with a size: one declared @opaque, for example, is refused where the intrinsic is instantiated for it. The atomic operations and an integer's bit operations accept fewer types still, stated with them.
Fallback bodies#
Where the registry allows it, the declaration may give a body written in Cone:
pub fn @intrinsic readRaw[T](p *T) T {*p;}
The body is the fallback: a back end with no implementation of its own for the intrinsic runs it instead, expanded at each call as an inline function would be. It must mean exactly what the intrinsic means. An intrinsic that nothing written in Cone can express, such as sizeof, has no fallback, and a body for it is an error. The compiler's --intrinsic-fallback option runs every fallback body in place of the compiler's own implementation, so that the two can be tested to agree.
Today's LLVM back end implements every intrinsic itself. Those with a fallback body are readRaw, moveRaw, the atomic operations and an integer's bit operations.
The intrinsics#
Each but srcFile, srcLine, isDebugBuild, the provisional constants of the build and a GPU workgroup's barriers is declared in core's mem and called as mem.name[T](...) (traceRoots, which takes no type, as mem.traceRoots(...)). Where an argument is a pointer to T or a T, the type argument may be left for the call to infer: mem.finalize(p) for a p *Point. Several intrinsics take a raw pointer and trust the caller's promise about it. Misused, they break memory safety in ways the compiler cannot see, so a call to one belongs inside trust. That is marked below as "needs trust" (not yet enforced).
sizeof#
fn @intrinsic sizeof[T]() usize
The number of bytes from one T to the next in an array: its size, including the padding at its end that keeps the next one aligned. mem.sizeof[T]() * n is the memory n values need. A struct with no fields has size 0. The answer is a constant for the target: no code runs to compute it. It is not yet usable where the compiler itself needs a number, such as an array's length, because the compiler asks the target's layout for it when it generates code. No fallback.
alignof#
fn @intrinsic alignof[T]() usize
The alignment in bytes the target requires of a T in memory: always a power of two, at least 1, and sizeof[T]() is always a multiple of it. A struct's alignment is its strictest field's. A constant for the target, as sizeof is. No fallback.
needsFinal#
fn @intrinsic needsFinal[T]() bool
Whether finalize[T] does anything: true when T is an owning reference whose release does something (its region declares dealiasRef, or is Move), has a final method, has a field whose type needs finalizing (such an owning reference among them), is a tuple or an array whose element type does, or is an enum one of whose variants does: true for Option[String], false for Option[i32]. False for numbers, raw pointers, borrowed references, references into a region whose owners' going does nothing (a traced region's, whose collector frees its values), and structs of them. A constant. It lets a collection skip the loop over its elements when there is nothing to do. No fallback.
finalize#
fn @intrinsic finalize[T](p *T)
Runs the death of the T at p where it sits, exactly as a region-held value of type T dies, except that its memory is not given back: first T's own final, if it has one; then each field's death, in field order, recursively; then each owning reference its fields hold is released (which may finalize and free what that reference points at). If T is itself an owning reference, that reference is released: one owner goes away, and if it was the last, the value it points at dies. If T is an enum, the value dies as the variant it holds. If it is a tuple, each element dies in order; if it is a fixed-size array, each element dies in element order, first element first. For a type with nothing to finalize (needsFinal[T]() is false) it does nothing.
Needs trust: p must point at a live, initialized T, aligned for it. Afterwards that T is dead: reading it as a T or finalizing it again is a program error the compiler cannot detect. No fallback: the finalizer the compiler builds for T is the implementation.
typeRecord#
fn @intrinsic typeRecord[T]() *TypeRecord
The address of T's type record, a constant holding what library code needs to handle a T it cannot name the type of: a region that keeps values of many types, and must later finalize each. TypeRecord is a struct core declares, so it is a name of every module:
pub struct TypeRecord {
pub size usize; // sizeof[T]()
pub align usize; // alignof[T]()
pub finalize &fn(p *u8); // what finalize[T] does to the T at p
pub trace &fn(p *u8, mode u32); // what trace[T] does to the T at p
pub flags u32; // bit 0: needsFinal[T](); bit 1: holdsTraced[T]()
}
size and align are what sizeof and alignof say. finalize is a function that, called with the address of a T, does exactly what finalize[T] does to it, with the same needs: a live, initialized T there, dead afterwards. Where T needs no finalizing it is a function that does nothing, and bit 0 of flags is clear, so a caller can skip the call. trace is a function that, called with the address of a T and a mode, does exactly what trace[T] does to it: what a tracing collector calls to find the traced references a value holds. Where T holds none it does nothing, and bit 1 of flags is clear. Neither function pointer is ever null.
imm rec = mem.typeRecord[Point]() as &TypeRecord; imm fin = rec.finalize; fin(p as *u8); // p's Point dies where it sits
Asking twice in one compiled object gives the same address; nothing says two objects share one record, so records should not be compared across them. A region can also be handed the record of every value it allocates, by declaring its alloc to take one (Custom Allocators). A constant. No fallback.
holdsTraced#
fn @intrinsic holdsTraced[T]() bool
Whether a T needs tracing: whether it holds a traced reference, that is whether T is a reference into a region that declares Traced (Custom Allocators), or a struct, tuple, fixed-size array or enum holding one inline, or an Rc or So reference to a value that holds one (a trace passes through an owner), or a type that declares GcTraceElements and gives references to values that do (a list of traced references). What a borrow or a raw pointer points at is not part of the value. A constant. No fallback.
trace#
fn @intrinsic trace[T](p *T, mode u32)
Hands each traced reference the T at p holds to its region's mark, with the slot it is stored in: a struct's fields in field order, recursively; a tuple's elements in order; a fixed-size array's elements in element order; an enum's fields as the variant it holds; the value an Rc or So owner holds (an Rc's once for each mode, which is its visit's epoch); and the values a type declaring GcTraceElements holds, in the order its cursor gives them. A reference that is null (an Option holding None) is passed over. Where the region's mark takes them, it is also handed the reference's permission and mode, which means whatever the region's collector wants it to: the compiler only passes it on. For a type holding no traced reference (holdsTraced[T]() is false) it does nothing.
Needs trust: p must point at a live, initialized T. No fallback: the walk the compiler builds for T is the implementation.
traceRoots#
fn @intrinsic traceRoots(mode u32)
Hands every traced reference the running functions hold on the stack to its region's mark, as trace hands those a value holds, with the same mode: what a collector starts its marking from. It takes no type parameter. The stack's traced references are every local and parameter whose type holds one (a reference into a traced region, or a struct, tuple, array or enum holding one where it sits), in every function that has not yet returned, and every temporary such a function is making: an allocation's result, a call's result, and a value read through a reference or made from a raw pointer, each kept until the same expression runs again or its function returns. So a reference that is only an argument being gathered for a call is found while an earlier argument's call runs, and the value of a traced allocation, made before its region's alloc is called, is found while alloc runs. A borrowed reference is not traced: what it borrows from is held by its owner, which is. Where more than one of them holds a reference, mark is called for each. A function holding no traced reference costs nothing and is passed over; a value reached only through another object is not a root.
No fallback: the frames the compiler links in each function holding traced references are the implementation, walked by the C library the compiler links with.
sliceFromParts and sliceFromPartsMut#
fn @intrinsic sliceFromParts[T](p *T, len usize) &Array[T] fn @intrinsic sliceFromPartsMut[T](p *T, len usize) &mut Array[T]
A borrowed slice of the len consecutive T values starting at p: read-only from sliceFromParts, writable from sliceFromPartsMut. A len of 0 gives an empty slice, whatever p is.
Needs trust: the len values must be initialized, lie in one allocation, and stay there, unmoved and unfreed, for as long as the slice is used, which the compiler cannot see. A writable slice must be the only way those values are reached while it is used. No fallback.
readRaw#
fn @intrinsic readRaw[T](p *T) T
Moves the T at p out and returns it: the value is the caller's now, and the slot is left dead. Nothing at the slot is finalized, and p may be used again (for another slot, or to write this one). This differs from *p, which, for a move type, marks the variable p itself as moved.
Needs trust: p must point at a live, initialized T. Reading the same live value twice gives two owners of it, and it dies twice. Fallback: {*p;}.
writeRaw#
fn @intrinsic writeRaw[T](p *T, value T)
Moves value into the slot at p, which is treated as holding nothing: whatever was there is neither finalized nor released. This is how a value goes into memory that has just been allocated. *p = value is not the same: storing an owning reference that way releases the reference the slot is taken to hold, which in uninitialized memory is whatever bytes are there.
Where value is a construction by an init the type declares, mem.writeRaw[T](p, new T(args)), the init runs on the slot itself, its self being p, as an allocation's value is made in place: so an init that uses where its value is (an actor's, which sends to the actor through it) sees where the value stays.
Needs trust: p must be aligned for T and point into memory the caller may write. If the slot held a live value, that value is simply forgotten, never finalized. No fallback: nothing written in Cone stores without releasing.
A T holding a traced reference is refused, and so it is for moveRaw: memory placed this way is an arena's, a pool's or a collection's, which no collector traces. Where the call is in a generic's body, the refusal is reported where the program chose the type, as at List[Gc[mut, Node]].
moveRaw#
fn @intrinsic moveRaw[T](to *T, from *T, count usize)
Moves count consecutive values from from to to, as if each were read with readRaw and written with writeRaw. The two ranges may overlap, and the result is as if every value were read before any was written. Nothing is finalized: the values now live at to, and the slots of from that to does not cover are dead. A count of 0 does nothing.
Needs trust: both ranges must lie in memory the caller may use, and the values at from must be live. Fallback: a loop of readRaw and writeRaw, running upward when to is below from and downward otherwise. The LLVM back end does it as one block move.
srcFile and srcLine#
pub fn @intrinsic srcFile() &str pub fn @intrinsic srcLine() u32
Where the call is written: the name of its source file, as text, without the folders it is in, and its line, counting from 1. Each is a constant. A parameter whose default is srcFile() may be declared &str or &Array[u8], since any &str converts to a slice of bytes. Unlike the intrinsics above, these are declared at core's top level, not in mem, and every module calls them bare.
Written as a parameter's default value, each answers where the call taking the default is, each call its own place, not where the parameter is declared. This is how a function reports its caller's location, as panic and assert do, and any function may do the same, and hand the two on:
fn requirePositive(n i32, file &Array[u8] = srcFile(), line u32 = srcLine()) {
assert(n > 0, "must be positive", file, line);
}
requirePositive(-5); // panics naming this line
A parameter's default value is otherwise a literal; these two are the exception. An argument written for the parameter replaces the default, as for any. Written in an inline function's body, the place is in that body. No fallback: the place of a call is known only to the compiler.
Written in a macro's body, each answers where the macro is used, as Rust's file!() and line!() do. In a macro used inside another macro's body, that is where the outermost macro is used. An argument keeps its own place: a macro's argument is the source written at its use, and a call to either in it answers there. This is how core's assertDebug macro names its caller's line (Panics).
isDebugBuild#
pub fn @intrinsic isDebugBuild() bool
Whether this compile is a debug build: conec --debug, or build: debug in a build description, which is what Congo writes unless it is given --release. A constant, true or false for the whole compile. The side of an if on it that the build does not take is never generated, in a debug build as in a release one, so it costs nothing and none of it reaches the object:
if isDebugBuild() {checkEveryInvariant(&table);}
Like srcFile, it is declared at core's top level and called bare. Both sides are type checked in every build. No fallback: the build is known only to the compiler. The constants below are built the same way.
Constants of the build: a provisional mechanism; its final design is open#
Temporary. These let a package write code for one platform, or for a build the command line names, until the language has its own design for compile-time configuration. Their names, their forms and the flag may all change.
pub fn @intrinsic isWindows() bool pub fn @intrinsic isLinux() bool pub fn @intrinsic isMacOS() bool pub fn @intrinsic isWasm() bool pub fn @intrinsic isDefined(name &Array[u8]) bool pub fn @intrinsic definedInt(name &Array[u8]) i64
Each is a constant of the compile, as isDebugBuild is, declared at core's top level and called bare. The first four say which OS the compile targets, read from its target triple with no flag: the host's, or the one --triple or --wasm names. isWindows holds for a triple naming windows (x86_64-pc-windows-msvc), isLinux for one naming linux, isMacOS for one naming darwin or macos, and isWasm for a WebAssembly one; at most one holds, and none for a SPIR-V triple.
isDefined and definedInt read constants named on conec's command line: -D NAME defines NAME as 1, and -D NAME=123 as that integer (decimal or 0x hex, negative too), C's convention. A name given twice takes the last. isDefined answers whether the name was defined, and definedInt its integer, 0 for a name never defined. The name is written as a string literal, since it is looked up as the program compiles; anything else is an error. Congo passes -D to every compile of a build: congo run -D FAST -D LEVEL=2.
if isWindows() {SwitchToThread();} else {sched_yield();}
if isDefined("TRACE") {traceEveryMessage(&queue);}
imm level = definedInt("LEVEL");
An if whose condition is one of these, or !, and or or of them, has only the side the build takes generated. A call on the other side, to a function only another platform's libraries define, is never generated, so the program links without it. Both sides are still name resolved and type checked on every platform, so a name used only on one side must still be declared on every platform: an extern declaration of a function the target does not have is allowed, and costs nothing while nothing generated calls it. A comparison of one, definedInt("LEVEL") > 2, is not itself such a condition: both its sides are generated, though an optimized build may fold it.
These are core's, found bare through the prelude. A C-named module has no prelude, so it imports core and writes core.isWindows(); libc, which core itself imports, cannot, and so cannot choose a declaration by platform.
Atomic operations#
An atomic operation reads or changes the T at a pointer indivisibly: no other thread sees it half done, and when several threads operate on one location at once, each operation happens whole, one after another. They are the parts a thread-safe counter, flag or lock is built from. Each takes a raw pointer and so needs trust: p must point at a live, initialized T, aligned for it, which every thread reaches only through atomic operations while any of them may be writing.
Which types#
T is one of the types a target operates on indivisibly without a lock:
- an integer type of 8 to 64 bits, signed or unsigned, including usize and isize;
- bool;
- a raw pointer, to anything.
Each operation takes the part of that set it has a meaning for: atomicAdd, atomicSub, atomicMin and atomicMax an integer; atomicAnd, atomicOr and atomicXor an integer or a bool; the load, the store, the swap and the compare-swap all three. Anything else is an error where the operation is instantiated for it: a float, a struct, a tuple, an array, and every kind of reference, borrowed, owning or managed. A bool is operated on as the byte that holds it, which holds 0 or 1.
Orderings#
An atomic operation is indivisible by itself; what it promises about the memory around it is its ordering, on the C11 model. Core declares the orderings as an enum, MemOrder, a name of every module as mem is:
pub enum MemOrder u8 {
Relaxed, Acquire, Release, AcqRel, SeqCst;
}
- Relaxed: the operation is indivisible, and promises nothing about any other memory.
- Acquire, on an operation that loads: what this thread does after it is not seen to happen before it, so a thread that loads what another stored with Release sees everything the other did before that store.
- Release, on an operation that stores: what this thread did before it is not seen to happen after it.
- AcqRel, on a read-modify-write: both.
- SeqCst: both, as far as the operation loads and stores, and every SeqCst operation of the program also happens in one order that every thread sees. It is the one to use when in doubt.
The ordering is an ordinary argument, but the compiler requires it to be a constant, because which instruction the call becomes depends on it: a variant written where the call is, MemOrder.SeqCst, or a const holding one. A MemOrder held in a variable or passed as a parameter is an error at the call, even where its value never changes.
const acquire = MemOrder.Acquire; imm n = mem.atomicLoad[i32](p, acquire); // allowed imm o = MemOrder.Acquire; imm m = mem.atomicLoad[i32](p, o); // ERROR: not a constant
An ordering an operation cannot promise is an error at the call:
- a load (atomicLoad) has nothing to release, so it takes Relaxed, Acquire or SeqCst, not Release or AcqRel;
- a store (atomicStore) has nothing to acquire, so it takes Relaxed, Release or SeqCst, not Acquire or AcqRel;
- a read-modify-write, the swap included, takes all five;
- atomicCompareSwap takes two: the ordering when it stores, which may be any of the five, and the ordering when it does not, when it has only loaded. That failure ordering is Relaxed, Acquire or SeqCst, and no stronger than the success ordering: Acquire needs a success ordering of Acquire, AcqRel or SeqCst, and SeqCst needs SeqCst.
An ordering has no default: every call writes its own, since a parameter's default value must be a literal and a MemOrder is not one. What everyday code uses is core's Atomic[T], built on these operations, which gives SeqCst without writing it. There is no standalone fence: each ordering belongs to an operation.
atomicLoad and atomicStore#
fn @intrinsic atomicLoad[T](p *T, order MemOrder) T fn @intrinsic atomicStore[T](p *T, value T, order MemOrder)
atomicLoad answers the T at p; atomicStore puts value there. Neither finalizes or releases anything, which none of these types needs. Fallback: {*p;} and {*p = value;}.
atomicSwap#
fn @intrinsic atomicSwap[T](p *T, value T, order MemOrder) T
Puts value at p and answers what was there before, as one indivisible step. Fallback: a load, then a store.
atomicAdd, atomicSub, atomicAnd, atomicOr and atomicXor#
fn @intrinsic atomicAdd[T](p *T, value T, order MemOrder) T fn @intrinsic atomicSub[T](p *T, value T, order MemOrder) T fn @intrinsic atomicAnd[T](p *T, value T, order MemOrder) T fn @intrinsic atomicOr[T](p *T, value T, order MemOrder) T fn @intrinsic atomicXor[T](p *T, value T, order MemOrder) T
Each reads the T at p, combines it with value, stores the result there, and answers the value it read, the one from before, all as one indivisible step. Add and subtract wrap: a u8 holding 255 plus 1 holds 0, and an i32 holding its largest value plus 1 holds its smallest; neither is an error. And, or and exclusive or act on every bit, and on a bool are the logical operations. Fallback: a load, the operator (+, -, &, |, ^), then a store.
atomicMin and atomicMax#
fn @intrinsic atomicMin[T](p *T, value T, order MemOrder) T fn @intrinsic atomicMax[T](p *T, value T, order MemOrder) T
Each keeps at p the lesser, or the greater, of the T there and value, and answers the value from before, as one indivisible step. They compare as T does: signed for a signed integer, unsigned for an unsigned one, so a u8 holding 200 is greater than 3, and an i8 holding -56, the same bits, less. Fallback: a load, a comparison, and a store when value is the lesser (the greater).
atomicCompareSwap#
fn @intrinsic atomicCompareSwap[T](p *T, expected T, desired T,
success MemOrder, failure MemOrder) T, bool
If the T at p equals expected, stores desired there; either way, answers two values: the T it found at p, and whether it stored. Equal means ==, which for these types is the same bits: two pointers are equal when they hold the same address. It never fails spuriously: when the value found equals expected, it stores. The two results are a tuple, taken apart by assignment:
mut seen = 0i32;
mut stored = false;
seen, stored = mem.atomicCompareSwap[i32](p, 0i32, 1i32,
MemOrder.AcqRel, MemOrder.Acquire);
Fallback: a load, a comparison, and a store when they are equal.
What a fallback promises#
An atomic operation's fallback body is the plain operation, which is right only while one thread reaches p: it is what a back end with no atomic instructions of its own runs, and is correct for a program with one thread. The types and the orderings are checked the same way whichever runs.
How LLVM does it#
Each operation is one LLVM instruction, aligned as the type is: an atomic load or store, an atomicrmw (xchg, add, sub, and, or, xor, and min and max or umin and umax as T is signed or not), or a strong cmpxchg carrying both orderings. Relaxed is LLVM's monotonic. A bool is operated on as an i8. On a GPU each acts at the scope of the memory it changes, and SeqCst, which Vulkan does not have, is acquire-release (GPU Compute).
A GPU workgroup's barriers#
pub fn @intrinsic workgroupBarrier() {}
pub fn @intrinsic storageBarrier() {}
Every invocation of a compute workgroup waits at a barrier until all have reached it, and what each wrote before it, to workgroup memory or to the kernel's buffers, every other reads after it (GPU Compute). They are core's, outside mem, as isDebugBuild is. On a GPU each is SPIR-V's OpControlBarrier at the workgroup's scope, acquire-release over workgroup memory (workgroupBarrier) or, at the device's scope, over the buffers (storageBarrier). On the CPU each calls the running thread's barrier hook, which core's setBarrierHook sets and clearBarrierHook clears, and is nothing when the thread has none, as when a kernel runs one invocation at a time (GPU Compute, "Barriers on the CPU"); on WebAssembly each is nothing. The fallback body, empty, is what --intrinsic-fallback gives on every target.
An integer's bits#
Seven operations on an integer's bits. T is an integer type of 8 to 64 bits, usize and isize among them; any other type is refused where the intrinsic is instantiated for it. None needs trust. Each is also a method of every integer type, x.leadingZeros() for mem.leadingZeros(x), which the compiler builds with the type (Number Types). Each has a fallback body, which works a bit at a time.
countOnes, leadingZeros and trailingZeros#
fn @intrinsic countOnes[T](x T) u32 fn @intrinsic leadingZeros[T](x T) u32 fn @intrinsic trailingZeros[T](x T) u32
How many bits of x are 1; how many are 0 above its highest 1; how many are 0 below its lowest 1. A signed x is counted as its bits, the sign's among them. Defined for 0: leadingZeros and trailingZeros of 0 are the width, never undefined.
rotateLeft and rotateRight#
fn @intrinsic rotateLeft[T](x T, n u32) T fn @intrinsic rotateRight[T](x T, n u32) T
x's bits moved by n modulo the width, those shifted out of one end coming in at the other. Every n is defined, and one that is a multiple of the width gives x.
shlMasked and shrMasked#
fn @intrinsic shlMasked[T](x T, n u32) T fn @intrinsic shrMasked[T](x T, n u32) T
x shifted by n modulo the width, left or right: one instruction on most processors. shrMasked is arithmetic, filling with the sign bit, for a signed T, and logical, filling with zeros, for an unsigned one. They differ from the operators << and >> only when n is the width or more: there the operators shift every bit out (Shift operators), and these shift by what is left after taking whole widths off.
mulFold#
fn @intrinsic mulFold(a u64, b u64) u64
The full 128-bit product of a and b, its high 64 bits and its low 64 bits exclusive-or'd together: the mixing step of the wyhash family of hashes, which core's Hasher is built on (Hash). It is a function of two numbers and nothing else, and is one multiply on a 64-bit target. The low half alone is a * b, so the high half alone is mulFold(a, b) ^ (a * b). It is declared in mem and has a fallback body, which forms the product from 32-bit halves.
mem.mulFold(3u64, 5u64) // 15: the high half is 0 mem.mulFold(0xFFFFFFFFFFFFFFFFu64, 1u64) // 0xFFFFFFFFFFFFFFFF mem.mulFold(0xFFFFFFFFu64, 0xFFFFFFFFu64) // 0xFFFFFFFE00000001 mem.mulFold(0x8000000000000000u64, 2u64) // 1: the product is 2^64, high half 1, low half 0
How LLVM does them#
The counts are llvm.ctpop, llvm.ctlz and llvm.cttz, the last two told that 0 is not poison, their result resized to a u32; the rotates llvm.fshl and llvm.fshr with x as both halves; and the masked shifts a shl, ashr or lshr by the amount's low bits. mulFold extends both operands to 128 bits, multiplies, shifts the high half down and exclusive-ors the halves.