Skip to main content

Inst

Enum Inst 

Source
pub enum Inst {
Show 60 variants Unit { dst: Slot, }, Bool { dst: Slot, value: bool, }, Int { dst: Slot, value: i64, }, FuncRef { dst: Slot, callee: FunctionId, }, Tag { dst: Slot, layout: LayoutId, case: CaseId, }, Float { dst: Slot, bits: u64, }, Str { dst: Slot, text: StrId, }, Copy { dst: Slot, src: Slot, layout: LayoutId, }, Clear { slot: Slot, layout: LayoutId, }, Neg { num: Num, dst: Slot, a: Slot, }, Arith { num: Num, op: ArithOp, dst: Slot, a: Slot, b: Slot, }, Cmp { on: Compare, op: CmpOp, dst: Slot, a: Slot, b: Slot, }, ArithImm { op: ArithOp, dst: Slot, a: Slot, value: i64, }, CmpImm { op: CmpOp, dst: Slot, a: Slot, value: i64, }, Not { dst: Slot, a: Slot, }, Convert { to: Convert, dst: Slot, a: Slot, }, Jump { to: Pc, }, BranchFalse { cond: Slot, to: Pc, }, Switch { on: Slot, table: TableId, }, Return { src: Slot, }, Call { dst: Slot, callee: FunctionId, args: ArgsId, }, CallClosure { dst: Slot, closure: Slot, args: ArgsId, result: LayoutId, }, CallHost { dst: Slot, op: HostOpId, args: ArgsId, }, CallResource { dst: Slot, receiver: Slot, op: HostOpId, args: ArgsId, }, CallBuiltin { dst: Slot, builtin: BuiltinId, args: ArgsId, }, Alloc { dst: Slot, layout: LayoutId, len: Len, }, LoadField { dst: Slot, obj: Slot, at: u32, layout: LayoutId, }, StoreField { obj: Slot, at: u32, src: Slot, layout: LayoutId, }, LoadElem { dst: Slot, obj: Slot, index: Slot, layout: LayoutId, }, StoreElem { obj: Slot, index: Slot, src: Slot, layout: LayoutId, }, ByteAt { dst: Slot, obj: Slot, at: Slot, }, AllocBytes { dst: Slot, len: Slot, }, WriteByte { bytes: Slot, at: Slot, value: Slot, }, CopyBytes { args: ArgsId, }, FinishString { dst: Slot, bytes: Slot, }, AllocBuffer { dst: Slot, capacity: Slot, }, AppendByte { buffer: Slot, value: Slot, }, AppendBytes { args: ArgsId, }, FinishBuffer { dst: Slot, buffer: Slot, }, Len { dst: Slot, obj: Slot, }, LayoutOf { dst: Slot, obj: Slot, }, AddrOfSlot { dst: Slot, slot: Slot, }, AddrOfField { dst: Slot, obj: Slot, at: u32, }, AddrOfElem { dst: Slot, obj: Slot, index: Slot, layout: LayoutId, }, AddrOfPart { dst: Slot, addr: Slot, at: u32, }, Load { dst: Slot, addr: Slot, layout: LayoutId, }, Store { addr: Slot, src: Slot, layout: LayoutId, }, Box { dst: Slot, src: Slot, layout: LayoutId, }, Unbox { dst: Slot, src: Slot, layout: LayoutId, }, ScopeEnter { dst: Slot, name: StrId, }, ScopeLeave { scope: Slot, failed: Slot, error: Slot, layout: LayoutId, }, ScopeCancel { scope: Slot, }, Spawn { dst: Slot, scope: Slot, closure: Slot, answer: LayoutId, }, Await { dst: Slot, task: Slot, answer: LayoutId, }, Cancel { task: Slot, }, Settled { dst: Slot, src: Slot, answer: LayoutId, }, SharedLock { cell: Slot, }, SharedUnlock { cell: Slot, }, Trap { message: StrId, }, AssertFailed { message: Slot, },
}
Expand description

One instruction.

Variants§

§

Unit

dst = ()

Fields

§dst: Slot
§

Bool

dst = value

Fields

§dst: Slot
§value: bool
§

Int

dst = value, also how a Duration literal reaches a slot.

Fields

§dst: Slot
§value: i64
§

FuncRef

dst = <callee's dense id>, as a word.

What a lowered closure’s environment is given for its callee field, and the only place that value is produced. It is not Inst::Int, though the word it writes is the same FunctionId an Int of that value would be: a closure’s crate::layout::Shape::Closure already carries function: FunctionId as a typed fact, and writing the same id again through the untyped integer path made a second, uninspectable copy of it — one no verifier could tell from an ordinary integer, and one that renumbered every golden lowering with a closure in it whenever an unrelated function was added or moved, because the two facts were spelled a plain number in the listing rather than the name that number happened to hold that day.

crate::verify bounds callee against crate::program::Program::functions the way Inst::Call’s is bounded, and, where the destination is then stored into a statically-known closure object, checks that callee agrees with what the object’s own layout says — the comparison the two copies never had. crate::print renders it symbolically, by the callee’s name and not its number, which is what stops the churn: an unrelated declaration changing callee’s numeric value no longer changes a single character of the listing.

Fields

§dst: Slot
§callee: FunctionId
§

Tag

dst = <the index of caseinlayout>, as an enum’s discriminant.

The one way a discriminant is written. It is not Inst::Int, though the word it writes is the number an Int of that value would write, and the reason is Inst::FuncRef’s: a case index reached its slot through the untyped integer path, where no verifier could tell it from an ordinary number and nothing bounded it against the enum it was supposed to name.

Its destination is a Repr::Tag word, which is what makes the two facts separable at all: a tag is one non-reference word, physically an integer, and is refused by arithmetic, ordering and integer comparison because none of those accepts that Repr. Inst::Switch accepts it, and so do copying and clearing, which read a layout rather than a Repr.

crate::verify bounds case against layout’s own case list and refuses a layout that is not an enum. crate::print renders it by the case’s name, so an unrelated case added before it changes no character of a listing that does not mention it.

Fields

§dst: Slot
§layout: LayoutId
§case: CaseId
§

Float

dst = f64::from_bits(bits)

The bits rather than the f64 so that Inst can be Eq and Hashed, and so that a NaN in the source survives the IR unchanged.

Fields

§dst: Slot
§bits: u64
§

Str

dst = <the address of the string object for text>

The object already exists. ADR 0045 places every program literal in the heap before the run’s first instruction executes, so this is a load of a precomputed address — no branch, no allocation, no copy, whether this is the first turn of a loop or the millionth.

Fields

§dst: Slot
§text: StrId
§

Copy

dst = src, for the words layout describes.

This is ADR 0001’s field-wise shallow copy, and it is one operation because a value’s words are where the value is. Copying a Wrapper { p: Point, v: Vector } copies three words: the Point becomes independent because its words were copied, and the Vector stays shared because what was copied is its address. Both answers fall out of the same copy and neither needs a policy.

There is no sharing bit, no copy-on-write and no unsharing of a write path. Those were needed only while every struct was one address, and they existed to conceal an alias the representation had created.

let and var lower to the same thing: ADR 0001 says they do not change expression semantics, and Cove has no move semantics. A lowering may elide a copy whose source is a fresh temporary, but that is an optimisation — correctness never depends on proving uniqueness, and a lowering that cannot tell emits the copy.

Fields

§dst: Slot
§src: Slot
§layout: LayoutId
§

Clear

Zeroes the words layout describes at slot.

A slot whose value is dead. The lowering emits one at the end of the scope a binding belonged to, and at a temporary’s last use, for every slot whose Repr is Ref or Addr.

This is what keeps a static reference map from turning into a leak. The map says which slots the collector reads; it cannot say when the value in one stopped being needed, because that is a fact about a program point and the map is a fact about a function. Clearing the slot moves the answer into the data: a dead reference slot holds null, the collector reads null, and the object is unreachable at the next collection rather than at the next return.

It costs one store on a path that was going to leave the value behind anyway, and it is emitted only where the slot would otherwise retain something — never for a scalar, and never where the slot is about to be overwritten.

Fields

§slot: Slot
§layout: LayoutId
§

Neg

dst = -a

Fields

§num: Num
§dst: Slot
§

Arith

dst = a op b

Fields

§num: Num
§dst: Slot
§

Cmp

dst = a op b, answering a Bool.

Fields

§dst: Slot
§

ArithImm

dst = a op value, on Int words, where value was written in the source.

The same arithmetic Inst::Arith does — the same overflow, the same division and remainder by zero, the same Duration naming — with the right operand in the instruction instead of in a slot. It exists because the alternative is worse than a wasted word: the literals of a loop condition are materialised by instructions the back edge jumps over, so while i < 2000000 executed an Inst::Int two million times to write a constant into a temporary that nothing else ever read.

Two variants and not sixteen. op is a field here exactly as it is on Inst::Arith, so an operator added to the language costs no instruction, and this pair covers the eleven that exist.

The immediate is on the right, and only on the right. a - 1 and 1 - a are different questions and a % 7 and 7 % a more so, so a left-hand immediate would be a second family rather than a mirror of this one; a commutative operator’s lowering puts the literal on the right instead. There is no float immediate for the same reason there is no left one — a second family, for a form no benchmark asked for.

Num is absent because there is only Num::Int to name: value is an i64, and a Duration’s word is nanoseconds, which is an i64.

Fields

§dst: Slot
§value: i64
§

CmpImm

dst = a op value, comparing Int words, answering a Bool.

Inst::ArithImm’s other half, and Compare is absent for the reason Num is absent there: the operand is an Int word, so the comparison is Compare::Int.

Fields

§dst: Slot
§value: i64
§

Not

dst = !a

Fields

§dst: Slot
§

Convert

dst = <a, converted>

Fields

§dst: Slot
§

Jump

Continue at to.

Fields

§to: Pc
§

BranchFalse

Continue at to when cond is false; otherwise fall through.

One conditional branch rather than two: &&, ||, if and while all lower through it, and the lowering inverts the condition rather than the instruction set carrying both polarities.

Fields

§cond: Slot
§to: Pc
§

Switch

Continue at the entry of table selected by the Int in on.

This is how a match over an enum’s cases dispatches: on is the case index read out of the object, and the table has one target per case plus a default.

Fields

§on: Slot
§table: TableId
§

Return

Leave the function, answering the value at src.

src is the first slot of that value location, and how many words follow it is crate::Function::returns — which is why a listing writes that layout on the slot, and the whole run with it: return s0..s2:Result.

Fields

§src: Slot
§

Call

dst = callee(args...)

The machine writes args[i] into the callee’s slot i and gives it a frame beginning at the end of this one. Nothing else happens: the argument list is static, the destination is declared, and there is no buffer between the two frames.

Fields

§dst: Slot
§callee: FunctionId
§args: ArgsId
§

CallClosure

dst = closure(args...), where closure holds a reference to a crate::Shape::Closure object.

The callee is the function id in the object’s first payload word, and its captures are copied into the slots after the parameters.

result is the layout of what the call answers, and it is the one operand here that is not read off the object at run time. Which body this enters is a run-time fact; how wide its answer is, is not. The checker settles a call through a value against the callee’s function type, so the answer’s type — and with it the run of words the destination has to be — is as static as any other call’s.

It is carried rather than looked up because there is nowhere to look: every other call names a callee the program declares — a FunctionId, a crate::HostOpId, a crate::BuiltinId — and the answer’s layout is read from that declaration. A closure call names a word in a slot. Without this field the destination’s width was known to the checker, thrown away by the lowering, and then unavailable to everything downstream: crate::verify could ask only that dst was a slot at all, the encoded verifier the same, and a listing had to print the head word’s Repr where every other call prints the run. A two-word answer written into the last slot of a frame was checked by nothing.

Fields

§dst: Slot
§closure: Slot
§args: ArgsId
§result: LayoutId
§

CallHost

dst = <host op>(args...)

This is a boundary: the arguments are materialised into public public Values, the host answers one, and the answer is written back into a word. It is the only place in ordinary execution where a Value exists.

Fields

§dst: Slot
§args: ArgsId
§

CallResource

dst = <host op>(*receiver, args...), addressed to the resource the Repr::Host word in receiver names.

The same boundary Inst::CallHost is, reached the other way a callee can be found. Call and CallClosure are already that pair on this side of the boundary — a callee named statically, and a callee in a slot — and a host resource’s operations are the same distinction one boundary further out: ADR 0013 gives the host the table of what is open, so files.Writer.writeLine is dispatched on the handle and not on the module the source wrote in front of it.

The receiver is an operand of its own rather than args[0], and that is the difference that decides there are two instructions here rather than a flag on one. An crate::Arg is a value location the boundary materialises, and the registry does not take the handle as an argument — HostRegistry::call_resource takes it as the thing being addressed and hands the host only what follows. So putting it in the list would mean materialising a name into a Value in order to take it apart again, and the argument list would no longer be the arguments.

Fields

§dst: Slot
§receiver: Slot
§args: ArgsId
§

CallBuiltin

dst = <builtin>(args...)

A builtin operates on words and heap objects directly. It is not a boundary and it does not materialise anything.

Fields

§dst: Slot
§builtin: BuiltinId
§args: ArgsId
§

Alloc

dst = <a new object of layout>

The payload is zeroed, so a reference field of a half-built object is null rather than garbage if a collection happens before it is filled in.

Fields

§dst: Slot
§layout: LayoutId
§len: Len
§

LoadField

dst = <the value at payload word atofobj>

One instruction for every fixed-position read there is: a struct field, an enum’s case index (at == 0) or payload word, a closure’s capture. The lowering computes at from the layout it knows statically; the machine bounds-checks it against the layout the object names, because a reference slot carries no layout of its own.

Fields

§dst: Slot
§obj: Slot
§at: u32
§layout: LayoutId
§

StoreField

<payload word atofobj> = src

Fields

§obj: Slot
§at: u32
§src: Slot
§layout: LayoutId
§

LoadElem

dst = obj[index], for an object whose elements are layout wide.

The stride is the element layout’s width, so an Array<Point> is a run of two-word elements rather than a run of addresses.

Fields

§dst: Slot
§obj: Slot
§index: Slot
§layout: LayoutId
§

StoreElem

obj[index] = src

Fields

§obj: Slot
§index: Slot
§src: Slot
§layout: LayoutId
§

ByteAt

dst = <byte atof the stringobj>, as an Int in 0..=255.

The one instruction that reaches inside a word. Everything else here addresses a value location or a payload word, because a word is what a frame and a heap object are made of — but a String’s payload is bytes, eight to a word, and the only shape that reads one is this.

It is an instruction and not a builtin, and the difference is the whole reason it exists. String.byteAt as a call-builtin measured 58 ns of which 48 ns was being a builtin call — the operands copied into a buffer, the operand array built, the dispatch by two strings, the answer written back — for work that is one payload word, a shift and a mask. benches/builtincall is where those two numbers are.

at is bounds-checked against the receiver’s byte length, and an offset outside it stops the run. That is String.sliceBytes’s rule and not Array.get’s: a byte offset out of range is one this type never handed out, where an index out of range is arithmetic a caller did about a sequence it can count. Answering an Option here would also be answering it eight times per word of a lexer’s inner loop, and the wrapper was measured at more than the read.

Fields

§dst: Slot
§obj: Slot
§at: Slot
§

AllocBytes

dst = <a new, zeroed byte run of len bytes>.

ADR 0051’s allocation. It always allocates crate::Program::bytes_layout — the one shape every run under construction shares — so unlike Inst::Alloc it carries no LayoutId of its own, for Inst::Str’s reason: a program-wide constant should not have to be named at every call site that always means the same one.

The payload is zeroed exactly as Inst::Alloc’s is, so a run that is collected before it is filled walks safely — not because a half-written byte is meaningful, but because crate::Shape::Bytes holds no references for the collector to chase either way.

len is a byte count and a run-time value, because the whole point of ADR 0051’s construction is a length computed by summing the pieces a join was given — a fixed length would have made this Inst::Alloc with a Len::Count instead. A negative or oversized len fails through the same “this run has no memory left” refusal every other allocation does.

Fields

§dst: Slot
§len: Slot
§

WriteByte

bytes[at] = value, one checked byte of a run under construction.

The scalar half of ADR 0051’s two write primitives, and deliberately the smaller one: it exists for a delimiter or an encoded scalar a lowering writes one at a time, not as how a join is expected to move text. Copying more than a handful of bytes through this would replace one native copy with as many dispatches as there are bytes, which is exactly the shape Inst::CopyBytes exists to avoid.

bytes must name a live crate::Shape::Bytes object — writing into a String is refused, because a String’s bytes are the invariant Inst::FinishString exists to establish and never to reopen. at is bounds-checked against the run’s declared length the same way Inst::ByteAt’s is, and value must be a byte, 0..=255: neither bound is optional here the way it would be reading back a value this run already produced, because this is the instruction that puts an arbitrary integer into memory another instruction will one day read back and trust.

Fields

§bytes: Slot
§at: Slot
§value: Slot
§

CopyBytes

A bulk range copy into a run under construction: dst[dst_at .. dst_at+len] = src[src_at .. src_at+len].

This is ADR 0051’s principal instruction — the one a join or a fused sliceBytes lowers to instead of sliceBytes -> Vector.push -> join’s hidden allocations — and the reason it exists at all is that a byte loop over Inst::WriteByte would multiply dispatch by the number of bytes moved, which ADR 0051’s “why a byte loop in IR is not enough” rejects. One instruction, one native run copy.

§What it costs, and what it is charged

One dispatch and one unit of work per payload word moved, which is ADR 0052’s “charged proportionally to the bytes or words examined”. A word is the unit because a word is what the memory moves.

The charge is not folded into the count of instructions dispatched. That number is a public observable — the debugger, the trace, the profile and cove-bench all report it, and crate::vm::profile’s own test asserts its per-opcode totals sum to it — so weighted work has a coordinate of its own.

The copy is made in bounded chunks with a safepoint between them, and that is not a refinement of the charge but the thing that makes it sound. A charge taken only after an arbitrarily large copy would let one instruction run arbitrarily far past a fuel or cancellation bound before anything looked, which ADR 0040’s S + T forbids. One chunk is one stride of work, so a stopped run gets no further than a stride past the bound whatever length it was given.

A collection may therefore happen with the destination half written. That is safe for the reason ADR 0051 gave for the run’s payload holding no references, and rooted for a second one: the caller has already synced, and both objects are named by frame slots this instruction read them out of, so the walk finds them where it finds every other live reference.

Inst::AllocBytes and Inst::FinishString are not charged this way and not chunked. Their bulk work is inside the allocator’s zeroing and inside one from_utf8 over a copy of the run, neither of which this could interrupt, and charging an operation that cannot be interrupted only makes its overshoot visible rather than bounded. They remain one unit each, which is what an ordinary Inst::Alloc of a large Array has always been.

src may be a String or another crate::Shape::Bytes run — a fused slice copies straight out of the run that produced it, without finishing it as a String first — but dst must always be a crate::Shape::Bytes run under construction: writing into a String is refused for Inst::WriteByte’s reason. Bounds are checked against both objects’ declared lengths rather than left to whatever the native copy routine happens to do with an out-of-range range.

§Why five operands live behind an ArgsId

An encoded instruction has room for three slot-sized operands and a payload, and this needs five: dst, dst_at, src, src_at and len. Rather than spend a fifth Inst variant or a second instruction pair to carry the overflow, this reuses the machinery a call’s argument list already is — ArgsId names a row of crate::Program::args, and a call already demonstrates that an arity larger than three operands is a solved problem in this format. The row holds exactly five crate::Args, in the order dst, dst_at, src, src_at, len, and carries each one’s layout the same way a call’s arguments do, so the verifier checks them by the same rule rather than by a new one.

Fields

§args: ArgsId
§

FinishString

dst = <the run at bytes, validated and turned into an immutable String, in place>.

The instruction ADR 0051 closes construction with. bytes must name a live crate::Shape::Bytes run; its packed payload is read and checked as UTF-8 exactly once, because a run assembled from Inst::WriteByte and Inst::CopyBytes may hold anything a byte can hold, and ADR 0051 refuses to skip that check for an arbitrary run. Invalid UTF-8 fails with the same error a source-level string operation already raises for it.

On success the run becomes the answer without copying its payload: a crate::Shape::Bytes object and a crate::Shape::Str object of the same byte length occupy the same number of words, so finishing is a re-label of the object’s header — its layout changes from crate::Program::bytes_layout to crate::Program::str_layout and its len does not change at all — rather than an allocation and a copy. Not copying the payload is the whole performance argument this ADR makes: every byte a join moves is moved once, by Inst::CopyBytes, and finishing moves none of them again.

Fields

§dst: Slot
§bytes: Slot
§

AllocBuffer

dst = <a new, empty byte buffer whose store has room for capacity bytes>.

ADR 0052’s allocation, and the first of the four instructions that replace Inst::AllocBytes wherever the final length is not known before the writes. ADR 0051’s fixed run is enough when it is known; it is not enough for examples/covefmt, whose three hot joins are filled by data-dependent loops and whose largest is a var out parameter passed through recursive calls.

Two objects are allocated, because that is what a stable owner is: the owner is crate::Program::buffer_layout, two payload words holding a logical length and a reference; the store is crate::Program::bytes_layout, the same packed run ADR 0051 already has, whose header length is the capacity. Neither layout is named here, for Inst::AllocBytes’s reason: both are program-wide constants, and a call site that always means the same one should not have to say so.

capacity is a hint and not a bound. Exceeding it grows the store rather than failing, so a tuning estimate cannot change what a program answers — which is the whole of ADR 0052’s “capacity is not an Array length”. A capacity below the runtime’s own floor is raised to it, and a negative or oversized one fails through the same “this run has no memory left” refusal every other allocation does.

Fields

§dst: Slot
§capacity: Slot
§

AppendByte

buffer.append(value), one checked byte onto the end of a buffer.

The scalar half of ADR 0052’s append pair, and Inst::WriteByte’s counterpart for a growable run — with the one difference that makes a buffer a buffer: there is no offset. A write goes at the logical length and the logical length becomes one more, so a caller never names a position and can never leave a hole below one.

It exists for a delimiter or an encoded scalar a lowering emits one at a time, not as how text is expected to move: copying a run of bytes through this would be as many dispatches as there are bytes, which is what Inst::AppendBytes is for.

buffer must name a live owner and value must be a byte, 0..=255, for Inst::WriteByte’s reason — this is an instruction that puts an arbitrary integer into memory that Inst::FinishBuffer will later read back and validate. Nothing is bounds-checked against the capacity, because there is no bound to check: a full store grows.

Fields

§buffer: Slot
§value: Slot
§

AppendBytes

A bulk range append: buffer.append(src[from .. to]).

ADR 0052’s principal instruction, and Inst::CopyBytes’s growable counterpart. One dispatch moves the whole range, for the reason ADR 0051 gave when it refused a byte loop in IR: a loop of Inst::AppendByte would multiply dispatch by the number of bytes.

src may be a String or a crate::Shape::Bytes run, which is what lets a fused slice copy straight out of the run or the string that produced it. The ADR’s own example is the optimisation this enables: sliceBytes(source, from, to) -> append becomes one checked append from that source range and never materialises the slice.

Where src is a String, from and to are checked to be character boundaries and not merely in range — the same check, in the same words, that String.sliceBytes makes. ADR 0052 requires it: “appendSlice checks the same bounds and UTF-8 boundaries as String.sliceBytes”. Without it a program could assemble a run of valid pieces that is not valid UTF-8, and discover it only at Inst::FinishBuffer, where the offset that did it is long gone. A crate::Shape::Bytes source is held to no such rule, because a run under construction is not claiming to be text.

§What it costs, and what it is charged

Inst::CopyBytes’s answer, unchanged: one unit of work per payload word moved, in bounded chunks with a safepoint between them, so a stopped run gets no further than a stride past the bound whatever length it was given. Growth is charged as the allocation it is.

The store is grown once, up front, for the whole range rather than per chunk. That is not only cheaper: a growth part way through would have to copy a prefix that the chunks before it had already written, and the one allocation before the first chunk is what keeps the copy a copy.

§Why four operands live behind an ArgsId

Inst::CopyBytes’s reason at one fewer operand: an encoded instruction has room for three slot-sized operands and this needs four — buffer, src, from and to. Rather than spend a second instruction to carry the overflow, this reuses the machinery a call’s argument list already is. The row holds exactly four crate::Args in the order buffer, src, from, to, and carries each one’s layout the way a call’s arguments do, so the verifier checks them by the same rule.

Fields

§args: ArgsId
§

FinishBuffer

dst = <the buffer at buffer, consumed, its store validated and relabelled into an immutable String>.

ADR 0052’s finish, and Inst::FinishString’s counterpart for a growable run. The bytes are read and checked as UTF-8 exactly once, because a run assembled from Inst::AppendByte may hold anything a byte can hold, and invalid UTF-8 fails with the same error a source-level string operation already raises for it.

What is validated and what is answered is the live prefix [0, length). A store is as long as the last growth made it, and ADR 0052’s “finishing reuses the store” is what happens to the rest: the store is relabelled from crate::Program::bytes_layout to crate::Program::str_layout with the logical length, and the words between the two lengths become a free block the next sweep folds back in. Nothing is copied, which is the same O(1) transition Vector.freeze() already makes for elements.

The owner is then emptied — length zero, store null — exactly as Vector.freeze() empties a vector, because finishing consumes. That the consumed buffer has no second live holder is cove_sema’s conservative local uniqueness proof and not something this machine can answer; what the machine keeps is the liveness check, so a buffer used after a finish is refused rather than read as an empty one.

crate::Shape::Bytes cannot cross a Cove call and neither can the owner cross the Host boundary, but the owner can cross a call, which is the whole reason it is a value rather than a raw run: the formatter’s fn emit(node: Tree, var out: StringBuilder) needs to pass a partly built string down a recursion.

Fields

§dst: Slot
§buffer: Slot
§

Len

dst = <obj's header length>: an element count, or a string’s bytes.

Fields

§dst: Slot
§obj: Slot
§

LayoutOf

dst = <the [LayoutId] in obj's header>, as an Int.

The other half of the header word Inst::Len reads, and it is here for the same reason: what an object is is a question the object answers at run time, from its own header, and a Ref slot carries no layout of its own.

It exists because a dispatch has to ask it. A dyn Trait value’s implementation is decided by the type behind it, and nothing static says which that is; the object’s header does. Reading it into a slot turns “which implementation” into an ordinary Inst::Switch over a table the lowering builds from the trait’s declared conformances, which is why there is no dispatch instruction — one general question about an object, answered with the control flow that is already here.

Fields

§dst: Slot
§obj: Slot
§

AddrOfSlot

dst = &frame[slot]

A place is one word. There is no place object, no place stack and no table of places; a var parameter is an ordinary slot whose Repr is Addr.

Fields

§dst: Slot
§slot: Slot
§

AddrOfField

dst = &<payload word atofobj>

The lowering keeps obj in a live reference slot for exactly the address’s live range, and clears that slot with Inst::Clear when the address dies — not unconditionally for the rest of the frame, which would retain the object across everything a long-running body does afterwards. The collector therefore needs no interior-pointer logic, and the heap does not move, so the address stays correct across a collection for as long as it is live and no longer.

Fields

§dst: Slot
§obj: Slot
§at: u32
§

AddrOfElem

dst = &obj[index], at a stride of layout’s width.

Fields

§dst: Slot
§obj: Slot
§index: Slot
§layout: LayoutId
§

AddrOfPart

dst = addr + at, a static word offset into the value at addr.

The one place instruction whose operand is itself a place, and what makes a place composable. A place is the address of the first word of a value location, so without this a var parameter could only name the whole of what it was given: p.y = 1 through a var p: Point had to load both words, write one and store both back — observationally the same on one thread, but not what the address was for — and f(var p.y) could not be lowered at all, because there was no way to form the address to pass.

at is a word offset within the value the address names, computed by the lowering from the layout the checker settled. It is the same arithmetic a field of an inline struct is, done to an address instead of to a slot number, and the answer is again the address of the first word of a value location — so it goes back through Inst::Load, Inst::Store or another of these with no second rule about what an address is.

Nothing checks at against the value’s extent, because a frame does not record one: what an address names is a fact about the instruction that formed it, and crate::verify says the same of Inst::Switch’s operand for the same reason.

Fields

§dst: Slot
§addr: Slot
§at: u32
§

Load

dst = *addr, for the words layout describes.

Fields

§dst: Slot
§addr: Slot
§layout: LayoutId
§

Store

*addr = src, for the words layout describes.

A nested write through a var parameter updates the destination words in place. There is nothing between the address and the words, which is what a place being an address of the first word of a value location buys.

Fields

§addr: Slot
§src: Slot
§layout: LayoutId
§

Box

dst = <a box holding the words of src, tagged layout>

What a value becomes when its static type is not known: dyn Trait, a Host result a schema declared Any, an expression the checker declined to type. One word in the slot either way.

Fields

§dst: Slot
§src: Slot
§layout: LayoutId
§

Unbox

dst = <the value inside the box in src>, trapping if its tag is not layout.

Fields

§dst: Slot
§src: Slot
§layout: LayoutId
§

ScopeEnter

dst = <a new task scope, open>

scope name { ... } binds one of these, and everything the Language Card says about a scope is a fact about the two instructions that leave it rather than about this one: concurrent work belongs to a task scope, and leaving the scope waits for or cancels its child tasks.

name is what the source bound it to. It is carried because a diagnostic quotes it — task 2 of scope requests — and by the time a scope is a word there is nothing else left that knows.

Fields

§dst: Slot
§name: StrId
§

ScopeLeave

Leave the scope in scope the way the body reached its end: wait for every child, and say whether one of them failed in a way the enclosing function has to pass on.

failed is a Bool. When it is true, error holds the Err payload of the first child whose value was one, at layout — and the lowering wraps it in the enclosing function’s own Err and returns it, which is exactly what ? would have done. A child that raised is not that: a runtime error is not a value, so this instruction fails with it and the two ways a child can end stay two things.

A discriminated outcome rather than an instruction carrying control flow, because where the failure goes is a fact about the function the scope was written in — which Err to build, and what to return — and the lowering is what holds those.

Fields

§scope: Slot
§failed: Slot
§error: Slot
§layout: LayoutId
§

ScopeCancel

Cancel every child of the scope in scope and wait for it to stop.

What an early exit from a scope’s body reaches: a return, a ?, a break or a continue that leaves it. Leaving a scope waits for or cancels its children whichever way it is left, so this is an obligation on every exit path exactly as Inst::Clear is, and the lowering emits one per open scope the jump leaves.

It answers nothing. A scope being left early is already leaving with something to say, and a child’s failure discovered on the way out would replace it with an unrelated one.

Fields

§scope: Slot
§

Spawn

dst = scope.spawn(closure), on a thread of its own.

answer is the layout of the value the body produces, and it is here because the answer needs somewhere to be before the thread exists: the machine allocates an object of that width and records its address in the scope’s table, so the answer is an object in the run’s one heap and a root of this task from the moment it can hold anything. Handing the words back through the thread instead would leave them in no store the collector walks for as long as the join took.

This returns once the thread exists and orders nothing else. ADR 0008’s amendment is explicit that whether the child has run an instruction by the time the next one here does is the operating system’s answer.

Fields

§dst: Slot
§scope: Slot
§closure: Slot
§answer: LayoutId
§

Await

dst = await task, for the words answer describes.

Waits for the task’s thread and answers the value its body produced. A body runs at most once and is waited for at most once, so awaiting the same handle twice answers the same value and repeats no effect.

Fields

§dst: Slot
§task: Slot
§answer: LayoutId
§

Cancel

task.cancel(): ask the task to stop at its next safepoint.

Asking is all it does. Whether the task stopped or had already finished is known only where something waits for it, which is why TaskCancelled is traced at the join and not here.

Fields

§task: Slot
§

Settled

dst = <a task already settled with the words at src>.

What a call to an async fn answers. The body ran at the call site, on this task’s stack, as Inst::Call; this is the handle the call hands back, and there is no thread anywhere in it.

That is the oracle’s reading rather than an invention here. Interpreter::call_target runs the body and wraps what it produced in crate::task::Task::settled, and crate::task::Task::settled’s own documentation says why: ADR 0008 gives a thread to spawn, which is where the language says concurrency begins, so nothing may depend on when an async fn body ran — only on the value an await produces. A call that is never awaited has still run.

So this task belongs to no scope and nothing joins it. It is position zero of crate::task::describe, which is the case that spelling exists for: this task, with no place in a spawn order to name. The words are copied into an object of the same shape a spawned task’s answer goes into, because an await reads the two the same way and a second arrangement would be a second thing to get right.

Fields

§dst: Slot
§src: Slot
§answer: LayoutId
§

SharedLock

Take the crate::Shape::Shared cell in cell, waiting for whoever holds it.

ADR 0008 makes lock the whole of a Shared’s access: there is no get and no set, so a read-modify-write cannot be written as two operations that race. What that means here is an ordinary Inst::CallClosure between this and Inst::SharedUnlock, with the address of the cell’s value as the closure’s argument — the same shape map is lowered to, and for the same reason docs/LINEAR_VM.md gives: a builtin never calls back into Cove. A builtin that ran the closure itself would put a Rust frame under every Cove frame it made.

So lock is two instructions rather than one that calls, and what the second one costs is an obligation: the release belongs to every exit path, exactly as Inst::Clear and Inst::ScopeCancel do. The lowering emits it on the path that finished, and a runtime error — which is not a jump the lowering can emit — is the machine’s to answer, once, for every cell the task was holding.

A task that asks for a cell it already holds is refused rather than made to wait, and that rule is untouched by ADR 0037: waiting would be waiting for itself, and no collector can answer a live lock state. What the ADR did remove is the other refusal — a closure that leaves the cell holding a handle to itself is an ordinary object-graph cycle now, collected when it becomes unreachable, so nothing here inspects what the closure left.

Fields

§cell: Slot
§

SharedUnlock

Give the cell in cell back, publishing everything written while it was held.

The lock word is the publication: it is taken with Acquire and released with Release, and every other word of the machine’s memory is relaxed and is allowed to be. Acquiring a cell therefore makes visible not only its own words but every object the previous holder allocated and stored into them.

Fields

§cell: Slot
§

Trap

Fail the run with message.

This is what an exhausted match and a failed Unbox reach. It is not a refusal to run the program: the program ran, and this is what it did.

Fields

§message: StrId
§

AssertFailed

Record that an assertion failed here, carrying the String in message.

The one instruction that writes nothing a program can read. An assertion is lowered rather than performed — see this crate’s lower::assertions — so by the time the failing arm runs, the Err(Error("assertion failed: ...")) is an ordinary value and the only thing left that the machine knows and the value does not is where it was written. A test runner points at the assertion the way every other error points at source, and this is how it is told.

The span is the instruction’s own, which is the assertion call’s, so nothing has to be threaded through the program to carry it. The message is a slot rather than a StrId because assertEqual renders the two values it compared and that string is built at run time; a runner compares it against the Err it is holding, so that a later unrelated failure is not reported at this assertion.

Fields

§message: Slot

Trait Implementations§

Source§

impl Clone for Inst

Source§

fn clone(&self) -> Inst

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Inst

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for Inst

Source§

fn eq(&self, other: &Inst) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl StructuralPartialEq for Inst

Auto Trait Implementations§

§

impl Freeze for Inst

§

impl RefUnwindSafe for Inst

§

impl Send for Inst

§

impl Sync for Inst

§

impl Unpin for Inst

§

impl UnsafeUnpin for Inst

§

impl UnwindSafe for Inst

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.