// viewFile is the content of view.json.
type viewFile struct {
- Chat viewState `json:"chat"`
- Compaction viewState `json:"compaction"`
+ Chat ViewState `json:"chat"`
+ Compaction ViewState `json:"compaction"`
}
// openMemory opens the chat in root and starts the builds that are ready. The
func (m *Memory) load() error {
data, err := os.ReadFile(m.viewPath())
if errors.Is(err, os.ErrNotExist) && m.log.Len() == 0 {
- m.chat = newView(chatHigh, chatLow)
- m.cview = newView(compactionHigh, compactionLow)
+ m.chat = newView(viewChatHigh, viewChatLow)
+ m.cview = newView(viewCompactionHigh, viewCompactionLow)
return nil
}
return fmt.Errorf("decode view: %w", err)
}
- m.chat, err = restoreView(file.Chat, chatHigh, chatLow, m.tree)
+ m.chat, err = restoreView(file.Chat, viewChatHigh, viewChatLow, m.tree)
if err != nil {
return fmt.Errorf("chat view: %w", err)
}
- m.cview, err = restoreView(file.Compaction, compactionHigh, compactionLow, m.tree)
+ m.cview, err = restoreView(file.Compaction, viewCompactionHigh, viewCompactionLow, m.tree)
if err != nil {
return fmt.Errorf("compaction view: %w", err)
}
_, err = memory.Append("echo", longText(0))
require.NoError(t, err)
require.NoError(t, memory.Wait(t.Context()))
- require.Equal(t, "<chat>\n0+1|"+pending+"\n</chat>", memory.View())
+ require.Equal(t, "<chat>\n0+1|"+viewPending+"\n</chat>", memory.View())
summarizer.SetFailing(false)
}()
batches := 0
- low := chatHigh
+ low := viewChatHigh
long := make([]bool, syntheticMessages)
for id := range syntheticMessages {
if id == syntheticMessages/2 {
defer memory.mutex.Unlock()
size := appendSize(memory.chat)
- require.LessOrEqual(t, size, chatHigh)
- require.LessOrEqual(t, appendSize(memory.cview), compactionHigh)
+ require.LessOrEqual(t, size, viewChatHigh)
+ require.LessOrEqual(t, appendSize(memory.cview), viewCompactionHigh)
merged := len(memory.chat.lines) < lines
if merged {
- require.LessOrEqual(t, size, chatLow)
+ require.LessOrEqual(t, size, viewChatLow)
}
requireOnlySummaries(t, memory, long, full)
if !built {
require.Equal(t, 0, line.level)
- require.Equal(t, head+pending, rendered[n+1])
+ require.Equal(t, head+viewPending, rendered[n+1])
continue
}
// appendSize returns the size of view with its last line as the pending mark.
func appendSize(view *View) int {
last := view.lines[len(view.lines)-1]
- return view.Size() - view.lineSize(last) + len(pending)
+ return view.Size() - view.lineSize(last) + len(viewPending)
}
// viewLines returns the number of lines in the chat view of memory.
return "", err
}
- text := pending
+ text := viewPending
if built {
text = strings.ReplaceAll(node.Text, "\n", " ")
}
// newTurnLoop returns the loop of agent with its memory. Its calls go through
// client, and notify gets each event. The clock gives the date of a turn.
func newTurnLoop(agent *Agent, memory *Memory, client TurnClient, clock func() time.Time, notify func(Event)) (*TurnLoop, error) {
- layout, err := loadLayout(filepath.Join(agent.dir, layoutFileName))
+ layout, err := loadViewLayout(filepath.Join(agent.dir, viewLayoutFileName))
if err != nil {
return nil, err
}
// of turnInput. The loop runs the tool calls of each response and sends their
// results back, until a response has no tool call: the final answer.
//
-// The view goes out as the messages of a viewLayout, so the first request of
-// a turn reads the view of the turn before from the prompt cache.
+// The view goes out as the messages of a ViewLayout, so the first request of a
+// turn reads the view of the turn before from the prompt cache.
//
// Each later request of a turn begins with the whole input of the request
// before it, then its output items, then the tool results. The output items
memory *Memory
client TurnClient
tools *toolbox
- layout *viewLayout
+ layout *ViewLayout
clock func() time.Time
notify func(Event)
wake chan struct{}
close(summarizer.gate)
test.events.WaitFor(t, "idle", 1)
require.Equal(t, 1, server.Calls())
- require.NotContains(t, turnInputTexts(t, server.Call(0))[0], pending)
+ require.NotContains(t, turnInputTexts(t, server.Call(0))[0], viewPending)
}
func TestInterruptEndsTheTurn(t *testing.T) {
)
const (
- // chatHigh and chatLow are the limits of the chat view in bytes.
- chatHigh = 128000
- chatLow = 64000
-
- // compactionHigh and compactionLow are the limits of the compaction
- // view in bytes.
- compactionHigh = 32000
- compactionLow = 16000
-
- // pending is the text of a line that is not built yet.
- pending = "(not summarized yet: zoom it)"
+ // viewChatHigh and viewChatLow are the limits of the chat view in
+ // bytes.
+ viewChatHigh = 128000
+ viewChatLow = 64000
+
+ // viewCompactionHigh and viewCompactionLow are the limits of the
+ // compaction view in bytes.
+ viewCompactionHigh = 32000
+ viewCompactionLow = 16000
+
+ // viewPending is the text of a line that is not built yet.
+ viewPending = "(not summarized yet: zoom it)"
)
-// nodeReader gives the built nodes of a tree.
-type nodeReader interface {
- Built(level, index int) (bool, error)
- Node(level, index int) (Node, bool, error)
-}
-
-// View is a list of tree nodes that covers messages 0 to end-1 without a gap,
-// oldest first. Each new message appends its leaf as a line. Once the size of
-// the view goes above high, one batch merges the most due sibling pairs until
-// the size is at most low. A batch merges only pairs whose parent is built. If
-// the batch cannot get to low, it continues at each new message until it does.
-//
-// The view keeps the nodes of its built lines in memory, so its size and its
-// text need no read of the tree. The size of a line is the size of its node
-// text, or the size of the pending text when the node is not built.
-//
-// For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the
-// number of messages and last the ID of the last message of the pair. That is
-// how long ago the pair ended, measured in its own line size. With a budget of
-// the length of the rollback push list of the reference design, this order
-// makes the same merges as the push. The oldest pair wins a tie.
-type View struct {
- high int
- low int
- lines []nodeKey
- nodes map[nodeKey]Node
- end int
- merging bool
-}
-
-// viewState is the saved form of a View. The limits are not part of it.
-type viewState struct {
- Lines [][2]int `json:"lines"`
- Merging bool `json:"merging"`
-}
-
// newView returns an empty view with the limits high and low.
func newView(high, low int) *View {
return &View{
// restoreView returns the view of state with the limits high and low. Each line
// must continue the one before it, and each line above level 0 must be built.
-func restoreView(state viewState, high, low int, tree nodeReader) (*View, error) {
+func restoreView(state ViewState, high, low int, tree ViewNodeReader) (*View, error) {
v := newView(high, low)
v.merging = state.Merging
for _, pair := range state.Lines {
return v, nil
}
-// load keeps the node of line when it is built, and tells if it is.
-func (v *View) load(tree nodeReader, line nodeKey) (bool, error) {
- node, built, err := tree.Node(line.level, line.index)
- if err != nil || !built {
- return false, err
- }
-
- v.nodes[line] = node
-
- return true, nil
+// View is a list of tree nodes that covers messages 0 to end-1 without a gap,
+// oldest first. Each new message appends its leaf as a line. Once the size of
+// the view goes above high, one batch merges the most due sibling pairs until
+// the size is at most low. A batch merges only pairs whose parent is built. If
+// the batch cannot get to low, it continues at each new message until it does.
+//
+// The view keeps the nodes of its built lines in memory, so its size and its
+// text need no read of the tree. The size of a line is the size of its node
+// text, or the size of the pending text when the node is not built.
+//
+// For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the
+// number of messages and last the ID of the last message of the pair. That is
+// how long ago the pair ended, measured in its own line size. With a budget of
+// the length of the rollback push list of the reference design, this order
+// makes the same merges as the push. The oldest pair wins a tie.
+type View struct {
+ high int
+ low int
+ lines []nodeKey
+ nodes map[nodeKey]Node
+ end int
+ merging bool
}
// State returns the saved form of the view.
-func (v *View) State() viewState {
- state := viewState{
+func (v *View) State() ViewState {
+ state := ViewState{
Lines: make([][2]int, len(v.lines)),
Merging: v.merging,
}
// Append adds the leaf of the next message as a line and merges when the view
// is too large. It tells if a merge changed the view.
-func (v *View) Append(tree nodeReader) (bool, error) {
+func (v *View) Append(tree ViewNodeReader) (bool, error) {
line := nodeKey{0, v.end}
_, err := v.load(tree, line)
if err != nil {
return v.fit(tree)
}
-// fit merges the most due pairs while a batch runs, and tells if a merge
-// changed the view.
-func (v *View) fit(tree nodeReader) (bool, error) {
- size := v.Size()
- if size > v.high {
- v.merging = true
- }
-
- merged := false
- for v.merging && size > v.low {
- best, err := v.mostDue(tree)
- if err != nil || best < 0 {
- return merged, err
- }
-
- left, right := v.lines[best], v.lines[best+1]
- parent := nodeKey{left.level + 1, left.index / 2}
- _, err = v.load(tree, parent)
- if err != nil {
- return merged, err
- }
-
- size += v.lineSize(parent) - v.lineSize(left) - v.lineSize(right)
- delete(v.nodes, left)
- delete(v.nodes, right)
- v.lines[best] = parent
- v.lines = append(v.lines[:best+1], v.lines[best+2:]...)
- merged = true
- }
-
- v.merging = false
-
- return merged, nil
-}
-
// Coarsen takes each merge of other, a view of the same messages, and then
// merges in one batch until the size is at most low. Each line of the result is
// the coarser of the two lines that start at its first message. Dyadic ranges
// either nest or do not overlap, so the result covers the messages without a
// gap.
-func (v *View) Coarsen(other *View, tree nodeReader) error {
+func (v *View) Coarsen(other *View, tree ViewNodeReader) error {
lines := make([]nodeKey, 0, len(v.lines))
nodes := make(map[nodeKey]Node, len(v.nodes))
mine, theirs := 0, 0
return err
}
-// mostDue returns the position of the left line of the most due sibling pair
-// whose parent is built, or -1 when there is no such pair.
-func (v *View) mostDue(tree nodeReader) (int, error) {
- best := -1
- bestDue := 0.0
- for n := 0; n+1 < len(v.lines); n++ {
- left, right := v.lines[n], v.lines[n+1]
- if left.index%2 != 0 || right.level != left.level || right.index != left.index+1 {
- continue
- }
-
- last := nodeEnd(right.level, right.index) - 1
- due := math.Ldexp(float64(v.end-last), -left.level)
- if best >= 0 && due <= bestDue {
- continue
- }
-
- built, err := tree.Built(left.level+1, left.index/2)
- if err != nil {
- return -1, err
- }
-
- if built {
- best = n
- bestDue = due
- }
- }
-
- return best, nil
-}
-
// Refresh keeps node when it is the node of a pending line.
func (v *View) Refresh(node Node) {
key := nodeKey{node.Level, node.Index}
}
}
-// Size returns the sum of the line sizes.
-func (v *View) Size() int {
- size := 0
- for _, line := range v.lines {
- size += v.lineSize(line)
- }
-
- return size
-}
-
-// lineSize returns the size of a view line.
-func (v *View) lineSize(line nodeKey) int {
- node, built := v.nodes[line]
- if !built {
- return len(pending)
- }
-
- return node.Size
-}
-
// Render returns the view as the model reads it: one line "id+n|text" for each
// node inside <chat> tags, with the newlines of the text as spaces.
func (v *View) Render() string {
return lines
}
+// Context returns the lines of the view before message end as the model reads
+// them. It stops at the first line that is not built, so a compaction never
+// sees a pending mark or a part of a message.
+func (v *View) Context(end int) string {
+ var text strings.Builder
+ text.WriteString("<chat>\n")
+ for _, line := range v.lines {
+ _, built := v.nodes[line]
+ if !built || nodeEnd(line.level, line.index) > end {
+ break
+ }
+
+ v.renderLine(&text, line)
+ }
+
+ text.WriteString("</chat>")
+
+ return text.String()
+}
+
+// load keeps the node of line when it is built, and tells if it is.
+func (v *View) load(tree ViewNodeReader, line nodeKey) (bool, error) {
+ node, built, err := tree.Node(line.level, line.index)
+ if err != nil || !built {
+ return false, err
+ }
+
+ v.nodes[line] = node
+
+ return true, nil
+}
+
+// fit merges the most due pairs while a batch runs, and tells if a merge
+// changed the view.
+func (v *View) fit(tree ViewNodeReader) (bool, error) {
+ size := v.Size()
+ if size > v.high {
+ v.merging = true
+ }
+
+ merged := false
+ for v.merging && size > v.low {
+ best, err := v.mostDue(tree)
+ if err != nil || best < 0 {
+ return merged, err
+ }
+
+ left, right := v.lines[best], v.lines[best+1]
+ parent := nodeKey{left.level + 1, left.index / 2}
+ _, err = v.load(tree, parent)
+ if err != nil {
+ return merged, err
+ }
+
+ size += v.lineSize(parent) - v.lineSize(left) - v.lineSize(right)
+ delete(v.nodes, left)
+ delete(v.nodes, right)
+ v.lines[best] = parent
+ v.lines = append(v.lines[:best+1], v.lines[best+2:]...)
+ merged = true
+ }
+
+ v.merging = false
+
+ return merged, nil
+}
+
// renderLine writes one view line, "id+n|text", and a newline.
func (v *View) renderLine(text *strings.Builder, line nodeKey) {
- body := pending
+ body := viewPending
node, built := v.nodes[line]
if built {
body = strings.ReplaceAll(node.Text, "\n", " ")
text.WriteByte('\n')
}
-// Context returns the lines of the view before message end as the model reads
-// them. It stops at the first line that is not built, so a compaction never
-// sees a pending mark or a part of a message.
-func (v *View) Context(end int) string {
- var text strings.Builder
- text.WriteString("<chat>\n")
+// Size returns the sum of the line sizes.
+func (v *View) Size() int {
+ size := 0
for _, line := range v.lines {
- _, built := v.nodes[line]
- if !built || nodeEnd(line.level, line.index) > end {
- break
+ size += v.lineSize(line)
+ }
+
+ return size
+}
+
+// mostDue returns the position of the left line of the most due sibling pair
+// whose parent is built, or -1 when there is no such pair.
+func (v *View) mostDue(tree ViewNodeReader) (int, error) {
+ best := -1
+ bestDue := 0.0
+ for n := 0; n+1 < len(v.lines); n++ {
+ left, right := v.lines[n], v.lines[n+1]
+ if left.index%2 != 0 || right.level != left.level || right.index != left.index+1 {
+ continue
}
- v.renderLine(&text, line)
+ last := nodeEnd(right.level, right.index) - 1
+ due := math.Ldexp(float64(v.end-last), -left.level)
+ if best >= 0 && due <= bestDue {
+ continue
+ }
+
+ built, err := tree.Built(left.level+1, left.index/2)
+ if err != nil {
+ return -1, err
+ }
+
+ if built {
+ best = n
+ bestDue = due
+ }
}
- text.WriteString("</chat>")
+ return best, nil
+}
- return text.String()
+// lineSize returns the size of a view line.
+func (v *View) lineSize(line nodeKey) int {
+ node, built := v.nodes[line]
+ if !built {
+ return len(viewPending)
+ }
+
+ return node.Size
+}
+
+// ViewState is the saved form of a View. The limits are not part of it.
+type ViewState struct {
+ Lines [][2]int `json:"lines"`
+ Merging bool `json:"merging"`
+}
+
+// ViewNodeReader gives the built nodes of a tree.
+type ViewNodeReader interface {
+ Built(level, index int) (bool, error)
+ Node(level, index int) (Node, bool, error)
}
"strings"
)
-// layoutFileName is the file of the view layout in the directory of an agent.
-const layoutFileName = "layout.json"
-
-// viewLayout splits the view of a turn request into user messages that only
-// grow between batches. The view goes out as the same messages as in the
-// request before, while their lines give the same text, and the new lines
-// follow as one new message. So the end of the view of each earlier turn stays
-// a message end. On the ChatGPT backend, the first request of a turn with this
-// layout read the view of the turn before from the prompt cache in each of 10
-// turns, and with the view in one message it missed once in 5.
-//
-// The first message starts with "<chat>\n". The closing tag goes into the
-// state message after the view, so that no view message changes when lines
-// come. The layout keeps the line count and a hash of the text of each message
-// in layout.json, so it outlives a restart.
-type viewLayout struct {
- path string
- Messages []layoutMessage `json:"messages"`
-}
-
-// layoutMessage is one view message: its number of lines and the hash of its
-// text.
-type layoutMessage struct {
- Lines int `json:"lines"`
- Hash string `json:"hash"`
-}
-
-// loadLayout reads the layout at path. A missing file gives an empty layout.
-func loadLayout(path string) (*viewLayout, error) {
- layout := &viewLayout{
+// viewLayoutFileName is the file of the view layout in the directory of an
+// agent.
+const viewLayoutFileName = "layout.json"
+
+// loadViewLayout reads the layout at path. A missing file gives an empty
+// layout.
+func loadViewLayout(path string) (*ViewLayout, error) {
+ layout := &ViewLayout{
path: path,
}
return layout, nil
}
+// ViewLayout splits the view of a turn request into user messages that only
+// grow between batches. The view goes out as the same messages as in the
+// request before, while their lines give the same text, and the new lines
+// follow as one new message. So the end of the view of each earlier turn stays
+// a message end. On the ChatGPT backend, the first request of a turn with this
+// layout read the view of the turn before from the prompt cache in each of 10
+// turns, and with the view in one message it missed once in 5.
+//
+// The first message starts with "<chat>\n". The closing tag goes into the
+// state message after the view, so that no view message changes when lines
+// come. The layout keeps the line count and a hash of the text of each message
+// in layout.json, so it outlives a restart.
+type ViewLayout struct {
+ path string
+ Messages []ViewLayoutMessage `json:"messages"`
+}
+
// Split returns the view messages for lines and saves the layout. It keeps the
// earlier messages up to the first one whose text differs, and puts the rest
// of the lines into one new message.
-func (l *viewLayout) Split(lines []string) ([]string, error) {
+func (l *ViewLayout) Split(lines []string) ([]string, error) {
var texts []string
- var kept []layoutMessage
+ var kept []ViewLayoutMessage
start := 0
for _, message := range l.Messages {
if start+message.Lines > len(lines) {
}
text := viewMessage(len(texts) == 0, lines[start:start+message.Lines])
- if textHash(text) != message.Hash {
+ if viewTextHash(text) != message.Hash {
break
}
if start < len(lines) || len(texts) == 0 {
text := viewMessage(len(texts) == 0, lines[start:])
texts = append(texts, text)
- kept = append(kept, layoutMessage{
+ kept = append(kept, ViewLayoutMessage{
Lines: len(lines) - start,
- Hash: textHash(text),
+ Hash: viewTextHash(text),
})
}
return texts, nil
}
+// ViewLayoutMessage is one view message: its number of lines and the hash of
+// its text.
+type ViewLayoutMessage struct {
+ Lines int `json:"lines"`
+ Hash string `json:"hash"`
+}
+
// viewMessage returns the text of a view message with lines. The first one
// opens the chat.
func viewMessage(first bool, lines []string) string {
return text
}
-// textHash returns a short hash of text.
-func textHash(text string) string {
+// viewTextHash returns a short hash of text.
+func viewTextHash(text string) string {
sum := sha256.Sum256([]byte(text))
return hex.EncodeToString(sum[:16])
}
)
func TestViewLayoutKeepsTheMessagesItSent(t *testing.T) {
- path := filepath.Join(t.TempDir(), layoutFileName)
- layout, err := loadLayout(path)
+ path := filepath.Join(t.TempDir(), viewLayoutFileName)
+ layout, err := loadViewLayout(path)
require.NoError(t, err)
texts, err := layout.Split(nil)
require.Equal(t, []string{"<chat>\n", "0+1|a\n1+1|b\n"}, texts)
// The same lines give the same messages, also after a restart.
- layout, err = loadLayout(path)
+ layout, err = loadViewLayout(path)
require.NoError(t, err)
texts, err = layout.Split([]string{"0+1|a\n", "1+1|b\n"})
}
func TestViewLayoutStartsAgainAtAChangedMessage(t *testing.T) {
- layout, err := loadLayout(filepath.Join(t.TempDir(), layoutFileName))
+ layout, err := loadViewLayout(filepath.Join(t.TempDir(), viewLayoutFileName))
require.NoError(t, err)
_, err = layout.Split([]string{"0+1|a\n"})
)
func TestViewMergesInTheOrderOfTheRollbackPush(t *testing.T) {
- tree := builtTree{}
+ tree := ViewBuiltTree{}
view := newView(0, 0)
- var states *pushEntry
+ var states *ViewPushEntry
for tick := range 20001 {
- states = push(tick, states)
+ states = viewPush(tick, states)
starts := states.starts()
view.high = len(starts)
view.low = len(starts)
}
func TestViewMergesInOneBatchFromHighToLow(t *testing.T) {
- tree := builtTree{}
+ tree := ViewBuiltTree{}
view := newView(10, 5)
sizes := make([]int, 0, 20)
for range 20 {
}
func TestViewMergesOnlyBuiltParents(t *testing.T) {
- tree := partTree{
+ tree := ViewPartTree{
sizes: map[nodeKey]int{
{0, 0}: 1,
{0, 1}: 1,
}
func TestViewRendersPendingLinesWithoutTheirMessage(t *testing.T) {
- tree := partTree{
+ tree := ViewPartTree{
sizes: map[nodeKey]int{
{0, 0}: 3,
{0, 1}: 3,
},
}
- view, err := restoreView(viewState{
+ view, err := restoreView(ViewState{
Lines: [][2]int{{1, 0}, {0, 2}},
- }, chatHigh, chatLow, tree)
+ }, viewChatHigh, viewChatLow, tree)
require.NoError(t, err)
- require.Equal(t, "<chat>\n0+2|aaa aaa\n2+1|"+pending+"\n</chat>", view.Render())
- require.Equal(t, 7+len(pending), view.Size())
+ require.Equal(t, "<chat>\n0+2|aaa aaa\n2+1|"+viewPending+"\n</chat>", view.Render())
+ require.Equal(t, 7+len(viewPending), view.Size())
}
func TestViewCoarsenTakesTheCoarserLines(t *testing.T) {
- view, err := restoreView(viewState{
+ view, err := restoreView(ViewState{
Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}, {0, 4}, {0, 5}},
- }, 100, 100, builtTree{})
+ }, 100, 100, ViewBuiltTree{})
require.NoError(t, err)
- other, err := restoreView(viewState{
+ other, err := restoreView(ViewState{
Lines: [][2]int{{0, 0}, {0, 1}, {1, 1}, {0, 4}, {0, 5}},
- }, 100, 100, builtTree{})
+ }, 100, 100, ViewBuiltTree{})
require.NoError(t, err)
- require.NoError(t, view.Coarsen(other, builtTree{}))
+ require.NoError(t, view.Coarsen(other, ViewBuiltTree{}))
require.Equal(t, []nodeKey{{1, 0}, {1, 1}, {0, 4}, {0, 5}}, view.lines)
view.low = 2
- require.NoError(t, view.Coarsen(other, builtTree{}))
+ require.NoError(t, view.Coarsen(other, ViewBuiltTree{}))
require.Equal(t, []nodeKey{{2, 0}, {1, 2}}, view.lines)
require.False(t, view.merging)
}
func TestViewContextStopsAtTheFirstPendingLine(t *testing.T) {
- tree := partTree{
+ tree := ViewPartTree{
sizes: map[nodeKey]int{
{0, 0}: 1,
{0, 1}: 1,
},
}
- view, err := restoreView(viewState{
+ view, err := restoreView(ViewState{
Lines: [][2]int{{1, 0}, {0, 2}, {0, 3}},
}, 100, 100, tree)
require.NoError(t, err)
}
func TestRestoreViewRejectsAGap(t *testing.T) {
- _, err := restoreView(viewState{
+ _, err := restoreView(ViewState{
Lines: [][2]int{{0, 0}, {0, 2}},
- }, chatHigh, chatLow, builtTree{})
+ }, viewChatHigh, viewChatLow, ViewBuiltTree{})
require.ErrorContains(t, err, "does not start at message 1")
}
-// pushEntry is one entry of the rollback state list of the reference design,
-// newest first. Under push alone, life stays 0, so the entry leaves it out.
-type pushEntry struct {
- keep bool
- state int
- older *pushEntry
-}
+// ViewBuiltTree is a tree with each node built, with 1 byte of text.
+type ViewBuiltTree struct{}
-// push is the push of rollback_state_list.js.
-func push(state int, states *pushEntry) *pushEntry {
- if states == nil {
- return &pushEntry{
- state: state,
- }
- }
+func (ViewBuiltTree) Built(level, index int) (bool, error) {
+ return true, nil
+}
- if !states.keep {
- return &pushEntry{
- keep: true,
- state: states.state,
- older: states.older,
- }
- }
+func (ViewBuiltTree) Node(level, index int) (Node, bool, error) {
+ return testNode(level, index, "a"), true, nil
+}
- return &pushEntry{
- state: state,
- older: push(states.state, states.older),
- }
+// ViewPushEntry is one entry of the rollback state list of the reference
+// design, newest first. Under push alone, life stays 0, so the entry leaves it
+// out.
+type ViewPushEntry struct {
+ keep bool
+ state int
+ older *ViewPushEntry
}
// starts returns the states of the list, oldest first.
-func (e *pushEntry) starts() []int {
+func (e *ViewPushEntry) starts() []int {
var starts []int
for entry := e; entry != nil; entry = entry.older {
starts = append(starts, entry.state)
return starts
}
-// builtTree is a tree with each node built, with 1 byte of text.
-type builtTree struct{}
-
-func (builtTree) Built(level, index int) (bool, error) {
- return true, nil
-}
-
-func (builtTree) Node(level, index int) (Node, bool, error) {
- return testNode(level, index, "a"), true, nil
-}
-
-// partTree is a tree with the built nodes of its sizes.
-type partTree struct {
+// ViewPartTree is a tree with the built nodes of its sizes.
+type ViewPartTree struct {
sizes map[nodeKey]int
}
-func (p partTree) Built(level, index int) (bool, error) {
+func (p ViewPartTree) Built(level, index int) (bool, error) {
_, built := p.sizes[nodeKey{level, index}]
return built, nil
}
-func (p partTree) Node(level, index int) (Node, bool, error) {
+func (p ViewPartTree) Node(level, index int) (Node, bool, error) {
size, built := p.sizes[nodeKey{level, index}]
if !built {
return Node{}, false, nil
return testNode(level, index, text), true, nil
}
+
+// viewPush is the push of rollback_state_list.js.
+func viewPush(state int, states *ViewPushEntry) *ViewPushEntry {
+ if states == nil {
+ return &ViewPushEntry{
+ state: state,
+ }
+ }
+
+ if !states.keep {
+ return &ViewPushEntry{
+ keep: true,
+ state: states.state,
+ older: states.older,
+ }
+ }
+
+ return &ViewPushEntry{
+ state: state,
+ older: viewPush(states.state, states.older),
+ }
+}