diff options
Diffstat (limited to 'pkg/ir.go')
| -rw-r--r-- | pkg/ir.go | 865 |
1 files changed, 865 insertions, 0 deletions
diff --git a/pkg/ir.go b/pkg/ir.go new file mode 100644 index 00000000..d9f984a7 --- /dev/null +++ b/pkg/ir.go @@ -0,0 +1,865 @@ +package pkg + +import ( + "bufio" + "bytes" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "io" + "iter" + "slices" + "strconv" + "sync" + "syscall" + "unique" + "unsafe" +) + +// wordSize is the boundary which binary segments are always aligned to. +const wordSize = 8 + +// alignSize returns the padded size for aligning sz. +func alignSize[T int | uint64](sz T) T { + return sz + (wordSize-(sz)%wordSize)%wordSize +} + +// panicToError recovers from a panic and replaces a nil error with the panicked +// error value. If the value does not implement error, it is re-panicked. +func panicToError(errP *error) { + r := recover() + if r == nil { + return + } + + if err, ok := r.(error); !ok { + panic(r) + } else if *errP == nil { + *errP = err + } +} + +// irCache implements [IRCache]. +type irCache struct { + // Artifact to [unique.Handle] of identifier cache. + artifact sync.Map + // Identifier free list, must not be accessed directly. + identPool sync.Pool +} + +// zeroIRCache returns the initialised value of irCache. +func zeroIRCache() irCache { + return irCache{ + identPool: sync.Pool{New: func() any { return new(extIdent) }}, + } +} + +// IRCache provides memory management and caching primitives for IR and +// identifier operations against [Artifact] implementations. +// +// The zero value is not safe for use. +type IRCache struct{ irCache } + +// NewIR returns the address of a new [IRCache]. +func NewIR() *IRCache { + return &IRCache{zeroIRCache()} +} + +// Inputs returns an iterator over direct and transitive inputs of an [Artifact] +// in randomised order. +func Inputs(a Artifact) iter.Seq2[Artifact, unique.Handle[ID]] { + ic := NewIR() + ic.Ident(a) + return func(yield func(Artifact, unique.Handle[ID]) bool) { + ic.artifact.Range(func(key, value any) bool { + return yield(key.(Artifact), value.(unique.Handle[ID])) + }) + } +} + +// IContext is passed to [Artifact.Params] and provides methods for writing +// values to the IR writer. It does not expose the underlying [io.Writer]. +// +// IContext is valid until [Artifact.Params] returns. +type IContext struct { + // Address of underlying irCache, should be zeroed or made unusable after + // [Artifact.Params] returns and must not be exposed directly. + ic *irCache + // Written to by various methods, should be zeroed after [Artifact.Params] + // returns and must not be exposed directly. + w io.Writer + // Optional [Artifact] to cureRes cache, replaces [IRKindIdent] with + // checksum values if non-nil. The pathname field may not be populated. + inputs map[Artifact]cureRes +} + +// irZero is a zero IR word. +var irZero [wordSize]byte + +// IRValueKind denotes the kind of encoded value. +type IRValueKind uint32 + +const ( + // IRKindEnd denotes the end of the current parameters stream. The ancillary + // value is interpreted as [IREndFlag]. + IRKindEnd IRValueKind = iota + // IRKindIdent denotes the identifier of a dependency [Artifact]. The + // ancillary value is reserved for future use. + IRKindIdent + // IRKindUint32 denotes an inlined uint32 value. + IRKindUint32 + // IRKindString denotes a string with its true length encoded in header + // ancillary data. Its wire length is always aligned to 8 byte boundary. + IRKindString + + irHeaderShift = 32 + irHeaderMask = 0xffffffff +) + +// String returns a user-facing name of k. +func (k IRValueKind) String() string { + switch k { + case IRKindEnd: + return "terminator" + case IRKindIdent: + return "ident" + case IRKindUint32: + return "uint32" + case IRKindString: + return "string" + default: + return "invalid kind " + strconv.Itoa(int(k)) + } +} + +// irValueHeader encodes [IRValueKind] and a 32-bit ancillary value. +type irValueHeader uint64 + +// encodeHeader returns irValueHeader encoding [IRValueKind] and ancillary data. +func (k IRValueKind) encodeHeader(v uint32) irValueHeader { + return irValueHeader(v)<<irHeaderShift | irValueHeader(k) +} + +// put stores h in b[0:8]. +func (h irValueHeader) put(b []byte) { + binary.LittleEndian.PutUint64(b[:], uint64(h)) +} + +// append appends the bytes of h to b and returns the appended slice. +func (h irValueHeader) append(b []byte) []byte { + return binary.LittleEndian.AppendUint64(b, uint64(h)) +} + +// IREndFlag is ancillary data encoded in the header of an [IRKindEnd] value and +// specifies the presence of optional fields in the remaining [IRKindEnd] data. +// Order of present fields is the order of their corresponding constants defined +// below. +type IREndFlag uint32 + +const ( + // IREndKnownChecksum denotes a [KnownChecksum] artifact. For an [IRKindEnd] + // value with this flag set, the remaining data contains the [Checksum]. + IREndKnownChecksum IREndFlag = 1 << iota +) + +// mustWrite writes to IContext.w and panics on error. The panic is recovered +// from by the caller and used as the return value. +func (i *IContext) mustWrite(p []byte) { + if _, err := i.w.Write(p); err != nil { + panic(err) + } +} + +// WriteIdent writes the identifier of [Artifact] to the IR. The behaviour of +// WriteIdent is not defined for an [Artifact] not part of the slice returned by +// [Artifact.Inputs]. +func (i *IContext) WriteIdent(a Artifact) { + buf := i.ic.getIdentBuf() + defer i.ic.putIdentBuf(buf) + + IRKindIdent.encodeHeader(0).put(buf[:]) + if i.inputs != nil { + res, ok := i.inputs[a] + if !ok { + panic(InvalidLookupError(i.ic.Ident(a).Value())) + } + *(*ID)(buf[wordSize:]) = res.checksum.Value() + } else { + *(*ID)(buf[wordSize:]) = i.ic.Ident(a).Value() + } + i.mustWrite(buf[:]) +} + +// WriteUint32 writes a uint32 value to the IR. +func (i *IContext) WriteUint32(v uint32) { + i.mustWrite(IRKindUint32.encodeHeader(v).append(nil)) +} + +// irMaxStringLength is the maximum acceptable wire size of [IRKindString]. +const irMaxStringLength = 1 << 24 + +// IRStringError is a string value too big to encode in IR. +type IRStringError string + +func (IRStringError) Error() string { + return "params value too big to encode in IR" +} + +// Write writes p as a string value to the IR. +func (i *IContext) Write(p []byte) { + sz := alignSize(len(p)) + if len(p) > irMaxStringLength || sz > irMaxStringLength { + panic(IRStringError(p)) + } + + i.mustWrite(IRKindString.encodeHeader(uint32(len(p))).append(nil)) + i.mustWrite(p) + + psz := sz - len(p) + if psz > 0 { + i.mustWrite(irZero[:psz]) + } +} + +// WriteString writes s as a string value to the IR. +func (i *IContext) WriteString(s string) { + p := unsafe.Slice(unsafe.StringData(s), len(s)) + i.Write(p) +} + +// Encode writes a deterministic, efficient representation of a to w and returns +// the first non-nil error encountered while writing to w. +func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) { + return ic.encode(w, a, nil) +} + +// encode implements Encode but replaces identifiers with their cured checksums +// for a non-nil ident. The pathname field is unused. Caller must acquire +// Cache.identMu. +func (ic *irCache) encode( + w io.Writer, + a Artifact, + inputs map[Artifact]cureRes, +) (err error) { + deps := a.Inputs() + idents := make([]*extIdent, len(deps)) + if inputs == nil { + for i, d := range deps { + dbuf, did := ic.unsafeIdent(d, true) + if dbuf == nil { + dbuf = ic.getIdentBuf() + binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind())) + *(*ID)(dbuf[wordSize:]) = did.Value() + } else { + ic.storeIdent(d, dbuf) + } + defer ic.putIdentBuf(dbuf) + idents[i] = dbuf + } + } else { + for i, d := range deps { + res, ok := inputs[d] + if !ok { + return InvalidLookupError(ic.Ident(d).Value()) + } + + dbuf := ic.getIdentBuf() + binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind())) + *(*ID)(dbuf[wordSize:]) = res.checksum.Value() + defer ic.putIdentBuf(dbuf) + idents[i] = dbuf + } + } + slices.SortFunc(idents, func(a, b *extIdent) int { + return bytes.Compare(a[:], b[:]) + }) + idents = slices.CompactFunc(idents, func(a, b *extIdent) bool { + return *a == *b + }) + + // kind uint64 | rev uint64 | deps_sz uint64 + var buf [wordSize * 3]byte + binary.LittleEndian.PutUint64(buf[:], uint64(a.Kind())) + binary.LittleEndian.PutUint64(buf[wordSize:], GetRevision(a)) + binary.LittleEndian.PutUint64(buf[wordSize*2:], uint64(len(idents))) + if _, err = w.Write(buf[:]); err != nil { + return + } + + for _, dn := range idents { + // kind uint64 | ident ID + if _, err = w.Write(dn[:]); err != nil { + return + } + } + + func() { + i := IContext{ic, w, inputs} + + defer panicToError(&err) + defer func() { i.ic, i.w = nil, nil }() + + a.Params(&i) + }() + if err != nil { + return + } + + var f IREndFlag + kcBuf := ic.getIdentBuf() + sz := wordSize + if kc, ok := a.(KnownChecksum); ok { + f |= IREndKnownChecksum + *(*Checksum)(kcBuf[wordSize:]) = kc.Checksum() + sz += len(Checksum{}) + } + IRKindEnd.encodeHeader(uint32(f)).put(kcBuf[:]) + + _, err = w.Write(kcBuf[:sz]) + ic.putIdentBuf(kcBuf) + return +} + +// encodeAll implements EncodeAll by recursively encoding dependencies and +// performs deduplication by value via the encoded map. +func (ic *irCache) encodeAll( + w io.Writer, + a Artifact, + encoded map[Artifact]struct{}, +) (err error) { + if _, ok := encoded[a]; ok { + return + } + + for _, d := range a.Inputs() { + if err = ic.encodeAll(w, d, encoded); err != nil { + return + } + } + + encoded[a] = struct{}{} + return ic.Encode(w, a) +} + +// EncodeAll writes a self-describing IR stream of a to w and returns the first +// non-nil error encountered while writing to w. +// +// EncodeAll tries to avoid encoding the same [Artifact] more than once, however +// it will fail to do so if they do not compare equal by value, as that will +// require buffering and greatly reduce performance. It is therefore up to the +// caller to avoid causing dependencies to be represented in a way such that +// two equivalent artifacts do not compare equal. While an IR stream with +// repeated artifacts is valid, it is somewhat inefficient, and the reference +// [IRDecoder] implementation produces a warning for it. +// +// Note that while EncodeAll makes use of the ident free list, it does not use +// the ident cache, nor does it contribute identifiers it computes back to the +// ident cache. Because of this, multiple invocations of EncodeAll will have +// similar cost and does not amortise when combined with a call to Cure. +func (ic *irCache) EncodeAll(w io.Writer, a Artifact) error { + return ic.encodeAll(w, a, make(map[Artifact]struct{})) +} + +// ErrRemainingIR is returned for a [IRReadFunc] that failed to call +// [IRReader.Finalise] before returning. +var ErrRemainingIR = errors.New("implementation did not consume final value") + +// DanglingIdentError is an identifier in a [IRKindIdent] value that was never +// described in the IR stream before it was encountered. +type DanglingIdentError unique.Handle[ID] + +func (e DanglingIdentError) Error() string { + return "artifact " + Encode(unique.Handle[ID](e).Value()) + + " was never described" +} + +type ( + // IRDecoder decodes [Artifact] from an IR stream. The stream is read to + // EOF and the final [Artifact] is returned. Previous artifacts may be + // looked up by their identifier. + // + // An [Artifact] may appear more than once in the same IR stream. A + // repeating [Artifact] generates a warning via [Cache] and will appear if + // verbose logging is enabled. Artifacts may only depend on artifacts + // previously described in the IR stream. + // + // IRDecoder rejects an IR stream on the first decoding error, it does not + // check against nonzero reserved ancillary data or incorrectly ordered or + // redundant unstructured dependencies. An invalid IR stream as such will + // yield [Artifact] values with identifiers disagreeing with those computed + // by IRDecoder. For this reason, IRDecoder does not access the ident cache + // to avoid putting [Cache] into an inconsistent state. + // + // Methods of IRDecoder are not safe for concurrent use. + IRDecoder struct { + // Address of underlying [Cache], must not be exposed directly. + c *Cache + + // Underlying IR reader. Methods of [IRReader] must not use this as it + // bypasses ident measurement. + r io.Reader + // Artifacts already seen in the IR stream. + ident map[unique.Handle[ID]]Artifact + + // Whether Decode returned, and the entire IR stream was decoded. + done, ok bool + } + + // IRReader provides methods to decode the IR wire format and read values + // from the reader embedded in the underlying [IRDecoder]. It is + // deliberately impossible to obtain the [IRValueKind] of the next value, + // and callers must never recover from panics in any read method. + // + // It is the responsibility of the caller to call Finalise after all IR + // values have been read. Failure to call Finalise causes the resulting + // [Artifact] to be rejected with [ErrRemainingIR]. + // + // For an [Artifact] expected to have dependencies, the caller must consume + // all dependencies by calling Next until all dependencies are depleted, or + // call DiscardAll to explicitly discard them and rely on values encoded as + // [IRKindIdent] instead. Failure to consume all unstructured dependencies + // causes the resulting [Artifact] to be rejected with [MissedDependencyError]. + // + // Requesting the value of an unstructured dependency not yet described in + // the IR stream via Next, or reading an [IRKindIdent] value not part of + // unstructured dependencies via ReadIdent may cause the resulting + // [Artifact] to be rejected with [DanglingIdentError], however either + // method may return a non-nil [Artifact] implementation of unspecified + // value. + IRReader struct { + // Address of underlying [IRDecoder], should be zeroed or made unusable + // after finalisation and must not be exposed directly. + d *IRDecoder + // Common buffer for word-sized reads. + buf [wordSize]byte + + // Inputs sent before params, sorted by identifier. Resliced on + // each call to Next and checked to be depleted during Finalise. + inputs []*extIdent + + // Number of values already read, -1 denotes a finalised IRReader. + count int + // Header of value currently being read. + h irValueHeader + + // Measured IR reader. All reads for the current [Artifact] must go + // through this to produce a correct ident. + r io.Reader + // Buffers measure writes. Flushed and returned to d during Finalise. + ibw *bufio.Writer + } + + // IRReadFunc reads IR values written by [Artifact.Params] to produce an + // instance of [Artifact] identical to the one to produce these values. + IRReadFunc func(r *IRReader) Artifact +) + +// kind returns the [IRValueKind] encoded in h. +func (h irValueHeader) kind() IRValueKind { + return IRValueKind(h & irHeaderMask) +} + +// value returns ancillary data encoded in h. +func (h irValueHeader) value() uint32 { + return uint32(h >> irHeaderShift) +} + +// irArtifact refers to artifact IR interpretation functions and must not be +// written to directly. +var irArtifact = make(map[Kind]IRReadFunc) + +// InvalidKindError is an unregistered [Kind] value. +type InvalidKindError Kind + +func (e InvalidKindError) Error() string { + return "invalid artifact kind " + strconv.Itoa(int(e)) +} + +// register records the [IRReadFunc] of an implementation of [Artifact] under +// the specified [Kind]. Expecting to be used only during initialization, it +// panics if the mapping between [Kind] and [IRReadFunc] is not a bijection. +// +// register is not safe for concurrent use. register must not be called after +// the first instance of [Cache] has been opened. +func register(k Kind, f IRReadFunc) { + openMu.Lock() + defer openMu.Unlock() + + if opened { + panic("attempting to register after open") + } + if _, ok := irArtifact[k]; ok { + panic("attempting to register " + strconv.Itoa(int(k)) + " twice") + } + irArtifact[k] = f +} + +// Register records the [IRReadFunc] of a custom implementation of [Artifact] +// under the specified [Kind]. Expecting to be used only during initialization, +// it panics if the mapping between [Kind] and [IRReadFunc] is not a bijection, +// or the specified [Kind] is below [KindCustomOffset]. +// +// Register is not safe for concurrent use. Register must not be called after +// the first instance of [Cache] has been opened. +func Register(k Kind, f IRReadFunc) { + if k < KindCustomOffset { + panic("attempting to register within internal kind range") + } + register(k, f) +} + +// NewDecoder returns a new [IRDecoder] that reads from the [io.Reader]. +func (c *Cache) NewDecoder(r io.Reader) *IRDecoder { + return &IRDecoder{c, r, make(map[unique.Handle[ID]]Artifact), false, false} +} + +const ( + // irMaxValues is the arbitrary maximum number of values allowed to be + // written by [Artifact.Params] and subsequently read via [IRReader]. + irMaxValues = 1 << 12 + + // irMaxDeps is the arbitrary maximum number of direct dependencies allowed + // to be returned by [Artifact.Inputs] and subsequently decoded by + // [IRDecoder]. + irMaxDeps = 1 << 10 +) + +var ( + // ErrIRValues is returned for an [Artifact] with too many parameter values. + ErrIRValues = errors.New("artifact has too many IR parameter values") + + // ErrIRDepend is returned for an [Artifact] with too many dependencies. + ErrIRDepend = errors.New("artifact has too many dependencies") + + // ErrAlreadyFinalised is returned when attempting to use an [IRReader] that + // has already been finalised. + ErrAlreadyFinalised = errors.New("reader has already finalised") +) + +// enterReader panics with an appropriate error for an out-of-bounds count and +// must be called at some point in any exported method. +func (ir *IRReader) enterReader(read bool) { + if ir.count < 0 { + panic(ErrAlreadyFinalised) + } + if ir.count >= irMaxValues { + panic(ErrIRValues) + } + + if read { + ir.count++ + } +} + +// IRKindError describes an attempt to read an IR value of unexpected kind. +type IRKindError struct { + Got, Want IRValueKind + Ancillary uint32 +} + +func (e *IRKindError) Error() string { + return fmt.Sprintf( + "got %s IR value (%#x) instead of %s", + e.Got, e.Ancillary, e.Want, + ) +} + +// readFull reads until either p is filled or an error is encountered. +func (ir *IRReader) readFull(p []byte) (n int, err error) { + for n < len(p) && err == nil { + var nn int + nn, err = ir.r.Read(p[n:]) + n += nn + } + return +} + +// mustRead reads from the underlying measured reader and panics on error. If +// an [io.EOF] is encountered and n != len(p), the error is promoted to a +// [io.ErrUnexpectedEOF], if n == 0, [io.EOF] is kept as is, otherwise it is +// zeroed. +func (ir *IRReader) mustRead(p []byte) { + n, err := ir.readFull(p) + if err == nil { + return + } + + if errors.Is(err, io.EOF) { + if n == len(p) { + return + } + err = io.ErrUnexpectedEOF + } + panic(err) +} + +// mustReadHeader reads the next header via d and checks its kind. +func (ir *IRReader) mustReadHeader(k IRValueKind) { + ir.mustRead(ir.buf[:]) + ir.h = irValueHeader(binary.LittleEndian.Uint64(ir.buf[:])) + if wk := ir.h.kind(); wk != k { + panic(&IRKindError{wk, k, ir.h.value()}) + } +} + +// putAll returns all dependency buffers to the underlying [Cache]. +func (ir *IRReader) putAll() { + for _, buf := range ir.inputs { + ir.d.c.putIdentBuf(buf) + } + ir.inputs = nil +} + +// DiscardAll discards all unstructured dependencies. This is useful to +// implementations that encode dependencies as [IRKindIdent] which are read back +// via ReadIdent. +func (ir *IRReader) DiscardAll() { + if ir.inputs == nil { + panic("attempting to discard dependencies twice") + } + ir.putAll() +} + +// ErrDependencyDepleted is returned when attempting to advance to the next +// unstructured dependency when there are none left. +var ErrDependencyDepleted = errors.New("reading past end of dependencies") + +// Next returns the next unstructured dependency. +func (ir *IRReader) Next() Artifact { + if len(ir.inputs) == 0 { + panic(ErrDependencyDepleted) + } + + id := unique.Make(ID(ir.inputs[0][wordSize:])) + ir.d.c.putIdentBuf(ir.inputs[0]) + ir.inputs = ir.inputs[1:] + + if a, ok := ir.d.ident[id]; !ok { + ir.putAll() + panic(DanglingIdentError(id)) + } else { + return a + } +} + +// MissedDependencyError is the number of unstructured dependencies remaining +// in [IRReader] that was never requested or explicitly discarded before +// finalisation. +type MissedDependencyError int + +func (e MissedDependencyError) Error() string { + return "missed " + strconv.Itoa(int(e)) + " unstructured dependencies" +} + +var ( + // ErrUnexpectedChecksum is returned by a [IRReadFunc] that does not expect + // a checksum but received one in [IRKindEnd] anyway. + ErrUnexpectedChecksum = errors.New("checksum specified on unsupported artifact") + // ErrExpectedChecksum is returned by a [IRReadFunc] that expects a checksum + // but did not receive one in [IRKindEnd]. + ErrExpectedChecksum = errors.New("checksum required but not specified") +) + +// Finalise reads the final [IRKindEnd] value and marks r as finalised. Methods +// of r are invalid upon entry into Finalise. If a [Checksum] is available via +// [IREndKnownChecksum], its handle is returned and the caller must store its +// value in the resulting [Artifact]. +func (ir *IRReader) Finalise() (checksum unique.Handle[Checksum], ok bool) { + ir.enterReader(true) + ir.count = -1 + + ir.mustReadHeader(IRKindEnd) + f := IREndFlag(ir.h.value()) + + if f&IREndKnownChecksum != 0 { + buf := ir.d.c.getIdentBuf() + defer ir.d.c.putIdentBuf(buf) + + ir.mustRead(buf[wordSize:]) + checksum = unique.Make(Checksum(buf[wordSize:])) + ok = true + } + + if err := ir.ibw.Flush(); err != nil { + panic(err) + } + ir.r, ir.ibw = nil, nil + + if len(ir.inputs) != 0 { + panic(MissedDependencyError(len(ir.inputs))) + } + + return +} + +// ReadIdent reads the next value as [IRKindIdent]. +func (ir *IRReader) ReadIdent() Artifact { + ir.enterReader(true) + ir.mustReadHeader(IRKindIdent) + + buf := ir.d.c.getIdentBuf() + defer ir.d.c.putIdentBuf(buf) + + ir.mustRead(buf[wordSize:]) + id := unique.Make(ID(buf[wordSize:])) + + if a, ok := ir.d.ident[id]; !ok { + panic(DanglingIdentError(id)) + } else { + return a + } +} + +// ReadUint32 reads the next value as [IRKindUint32]. +func (ir *IRReader) ReadUint32() uint32 { + ir.enterReader(true) + ir.mustReadHeader(IRKindUint32) + return ir.h.value() +} + +// ReadStringBytes reads the next value as [IRKindString] but returns it as a +// byte slice instead. +func (ir *IRReader) ReadStringBytes() []byte { + ir.enterReader(true) + ir.mustReadHeader(IRKindString) + + sz := int(ir.h.value()) + szWire := alignSize(sz) + if szWire > irMaxStringLength { + panic(IRStringError("\x00")) + } + + p := make([]byte, szWire) + ir.mustRead(p) + return p[:sz] +} + +// ReadString reads the next value as [IRKindString]. +func (ir *IRReader) ReadString() string { + p := ir.ReadStringBytes() + return unsafe.String(unsafe.SliceData(p), len(p)) +} + +// A RevisionError describes the first [Artifact] in an IR stream claiming a +// revision not supported by the registered implementation. +type RevisionError [2]uint64 + +func (e RevisionError) Error() string { + return "claimed revision " + strconv.FormatUint(e[0], 10) + + " differs from implementation value " + strconv.FormatUint(e[1], 10) +} + +// decode decodes the next [Artifact] in the IR stream and returns any buffer +// originating from [Cache] before returning. decode returns [io.EOF] if and +// only if the underlying [io.Reader] is already read to EOF. +func (d *IRDecoder) decode() (a Artifact, err error) { + defer panicToError(&err) + var ir IRReader + + defer func() { ir.d = nil }() + ir.d = d + + h := sha512.New384() + ir.ibw = d.c.getWriter(h) + defer d.c.putWriter(ir.ibw) + ir.r = io.TeeReader(d.r, ir.ibw) + + if n, _err := ir.readFull(ir.buf[:]); _err != nil { + if errors.Is(_err, io.EOF) { + if n != 0 { + _err = io.ErrUnexpectedEOF + } + } + + err = _err + return + } + ak := Kind(binary.LittleEndian.Uint64(ir.buf[:])) + f, ok := irArtifact[ak] + if !ok { + err = InvalidKindError(ak) + return + } + + ir.mustRead(ir.buf[:]) + rev := binary.LittleEndian.Uint64(ir.buf[:]) + + defer ir.putAll() + ir.mustRead(ir.buf[:]) + sz := binary.LittleEndian.Uint64(ir.buf[:]) + if sz > irMaxDeps { + err = ErrIRDepend + return + } + ir.inputs = make([]*extIdent, sz) + for i := range ir.inputs { + ir.inputs[i] = d.c.getIdentBuf() + } + for _, buf := range ir.inputs { + ir.mustRead(buf[:]) + } + + a = f(&ir) + if a == nil { + err = syscall.ENOTRECOVERABLE + return + } + if _rev := GetRevision(a); _rev != rev { + err = RevisionError{rev, _rev} + return + } + + if ir.count != -1 { + err = ErrRemainingIR + return + } + + buf := d.c.getIdentBuf() + h.Sum(buf[wordSize:wordSize]) + id := unique.Make(ID(buf[wordSize:])) + d.c.putIdentBuf(buf) + if _, ok = d.ident[id]; !ok { + d.ident[id] = a + } else { + d.c.msg.Verbosef( + "artifact %s%s%s appeared more than once in IR stream", + d.c.sgrIdent, Encode(id.Value()), d.c.sgrRes, + ) + } + + return +} + +// Decode consumes the IR stream to EOF and returns the final [Artifact]. After +// Decode returns, Lookup is available and Decode must not be called again. +func (d *IRDecoder) Decode() (a Artifact, err error) { + if d.done { + panic("attempting to decode an IR stream twice") + } + defer func() { d.done = true }() + + var cur Artifact +next: + a, err = d.decode() + + if err == nil { + cur = a + goto next + } + + if errors.Is(err, io.EOF) { + a, err = cur, nil + d.ok = true + } + return +} + +// Lookup looks up an [Artifact] described by the IR stream by its identifier. +func (d *IRDecoder) Lookup(id unique.Handle[ID]) (a Artifact, ok bool) { + if !d.ok { + panic("attempting to look up artifact without full IR stream") + } + a, ok = d.ident[id] + return +} |
