diff --git a/.github/workflows/memci.yml b/.github/workflows/memci.yml index b5197aa..33f5f07 100644 --- a/.github/workflows/memci.yml +++ b/.github/workflows/memci.yml @@ -17,7 +17,12 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: "1.24" + # TinyGo 0.42 builds with Go 1.25 through 1.27 and refuses anything + # outside that window, so the two versions below move together. + go-version: "1.26" + - uses: acifani/setup-tinygo@v2 + with: + tinygo-version: "0.42.0" - uses: soypat/memci@main with: @@ -25,3 +30,4 @@ jobs: # bindiff lives in this repo, so measure with the copy under test # rather than installing a published one. bindiff: go run ./cmd/bindiff + tinygo: tinygo diff --git a/build/elfutil/dwarf_test.go b/build/elfutil/dwarf_test.go index 18069e4..c5b2905 100644 --- a/build/elfutil/dwarf_test.go +++ b/build/elfutil/dwarf_test.go @@ -131,8 +131,7 @@ func TestStreamedLineTableMatchesResident(t *testing.T) { sec := xdwarf.Sections{Str: bytes.NewReader(str), LineStr: bytes.NewReader(lineStr)} // The fixture's largest unit header wants 1867 bytes of aux, so 2048 is - // about as tight as this walk goes -- and tight is the point, since a small - // aux means a refill across nearly every read. + // about as tight as this walk goes. for _, aux := range []int{2048, 4096, 16384} { t.Run("aux="+itoa(aux), func(t *testing.T) { sec.Line = bytes.NewReader(raw) @@ -181,10 +180,9 @@ func TestStreamBufferTooSmall(t *testing.T) { var next int64 next, err = xdwarf.DecodeLineUnit(&u, sec, off, size, auxBuf) if err == nil { - // LineUnit.Rows reports no error of its own, so a rewind that - // happens mid-program is only visible on the next unit's decode. for range u.Rows { } + err = u.Err() } if err != nil { break diff --git a/build/xdwarf/line.go b/build/xdwarf/line.go index 5672e4b..4156044 100644 --- a/build/xdwarf/line.go +++ b/build/xdwarf/line.go @@ -102,9 +102,12 @@ type LineUnit struct { sec Sections hdr []byte // The unit header, at the front of aux. Inline strRefs index it. - win []byte // The tail of aux, the fill buffer VisitRows streams through. + win []byte // The tail of aux, the fill buffer Rows streams through. // The opcode stream, as absolute offsets within .debug_line. progStart, progEnd int64 + // err holds why the last [LineUnit.Rows] walk stopped early. An iterator + // cannot return one, so it is kept here for [LineUnit.Err]. + err error } // Row is one row of the line table matrix: the state of the line program at a @@ -316,7 +319,7 @@ func (u *LineUnit) appendStr(dst []byte, r strRef) ([]byte, error) { // appendCStr appends the NUL-terminated string at off to dst, reading straight // onto dst's own spare capacity so it needs no scratch of its own. That matters // because the aux tail, the obvious place for scratch, is simultaneously the -// window a [LineUnit.VisitRows] walk is streaming the opcode program through. +// window a [LineUnit.Rows] walk is streaming the opcode program through. func appendCStr(dst []byte, r io.ReaderAt, off int64) ([]byte, error) { if r == nil { return dst, errNoStrSection @@ -348,7 +351,7 @@ func appendCStr(dst []byte, r io.ReaderAt, off int64) ([]byte, error) { // aux is scratch owned by the caller and borrowed by dst until the next decode // into the same buffer. The unit's header is copied to its front, where dst's // directory and file tables point, and its tail becomes the fill buffer -// [LineUnit.VisitRows] streams the opcode program through. An aux too small to +// [LineUnit.Rows] streams the opcode program through. An aux too small to // hold both reports how large it needs to be. Passing the same dst and aux back // on the next call makes decoding allocation-free. func DecodeLineUnit(dst *LineUnit, sec Sections, off, size int64, aux []byte) (next int64, err error) { @@ -723,8 +726,19 @@ func (u *LineUnit) skipForm(c *cursor, f Form) error { return c.err } -// Rows is an [iter.Seq] iterator implementation oveer LineUnit rows. +// Err reports why the last [LineUnit.Rows] walk stopped, or nil if it ran to +// the end of the opcode program. A malformed unit, or a lookback window too +// small for the decoder to step back through, ends a walk early; because an +// [iter.Seq] cannot return an error, a caller that needs to tell a truncated +// walk from a complete one has to ask here. +// +// [LineUnit.Rows] clears it, so Err describes the most recent walk only. +func (u *LineUnit) Err() error { return u.err } + +// Rows is an [iter.Seq] iterator over the unit's line table rows. A walk that +// ends early leaves its reason in [LineUnit.Err]. func (u *LineUnit) Rows(yield func(r Row) bool) { + u.err = nil var c streamCursor c.config(u.sec.Line, u.win, u.progStart, u.progEnd, u.sec.byteOrder()) @@ -828,6 +842,9 @@ func (u *LineUnit) Rows(yield func(r Row) bool) { } } } + // Only reached by running the program out; every early return above is a + // caller stopping the walk, which leaves u.err nil as cleared on entry. + u.err = c.err } func (u *LineUnit) resetRow() Row { diff --git a/build/xdwarf/line_test.go b/build/xdwarf/line_test.go index 4bf0af2..86d8dfe 100644 --- a/build/xdwarf/line_test.go +++ b/build/xdwarf/line_test.go @@ -275,6 +275,39 @@ func TestRowsStop(t *testing.T) { if n != 3 { t.Errorf("visited %d rows after stopping at 3", n) } + // Stopping is the caller's own doing. Reporting it as an error would make + // every early exit look like a malformed unit. + if err := u.Err(); err != nil { + t.Errorf("stopping the walk reported an error: %s", err) + } +} + +// TestRowsErr pins the rest of the Err contract: a walk that reaches the end of +// the opcode program reports nothing, and each walk describes only itself. +func TestRowsErr(t *testing.T) { + sec, size := loadSections(t, "../../testdata/helloc.elf") + var u xdwarf.LineUnit + if _, err := xdwarf.DecodeLineUnit(&u, sec, 0, size, make([]byte, defaultAux)); err != nil { + t.Fatal(err) + } + rows := 0 + for range u.Rows { + rows++ + } + if rows == 0 { + t.Fatal("the unit yielded no rows") + } + if err := u.Err(); err != nil { + t.Fatalf("a complete walk reported an error: %s", err) + } + // A second walk over the same unit must start from a clean slate rather + // than inheriting the first walk's verdict. + for range u.Rows { + break + } + if err := u.Err(); err != nil { + t.Errorf("a second walk inherited an error: %s", err) + } } func TestDecodeLineUnitRejectsBadInput(t *testing.T) {