Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

bf_test.gno

6.25 Kb · 186 lines
  1package bf
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/x/vm/vmkit/v0"
  7	"gno.land/p/nt/uassert/v0"
  8)
  9
 10// hello is the classic 106 byte hello-world. It writes "Hello World", 11
 11// bytes: the original run.gno comment was optimistic about the trailing "!\n".
 12const hello = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------."
 13
 14// corpus is the set of programs every ladder level must agree on. Each one is
 15// here because it exercises a rewrite that could plausibly be wrong.
 16var corpus = []struct {
 17	name string
 18	src  string
 19	want string
 20}{
 21	{"empty", "", ""},
 22	{"comment only", "this is not brainfuck", ""},
 23	{"hello", hello, "Hello World"},
 24	{"clear idiom", "+++++[-]+++++++++++++++++++++++++++++++++++++++++++++++.", "/"},
 25	{"move idiom", "+++++++++++++++++++++++++++++++++++++++++++++++++++[->+<]>.", "3"},
 26	{"multiply idiom", "+++++[->++++++++++<]>++.", "4"},
 27	{"two targets", "+++[->+>++<<]>+++++++++++++++++++++++++++++++++++++++.>+++++++++++++++++++++++++++++++++++++++++.", "*/"},
 28	{"scan walks", ">+>+>+>+[<]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.", "H"},
 29	{"scan right", ">>>+++++++++<<<[>]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.", "?"},
 30	{"nested loops", "++[>++[>+<-]<-]>>+++++++++++++++++++++++++++++++++++++++++++++++++.", "5"},
 31	{"dead run", "+++--+++--+++++++++++++++++++++++++++++++++++++++++++.", "-"},
 32	{"skipped loop", "[++++++++++++++++++++]+++++++++++++++++++++++++++++++++++++++++++++++.", "/"},
 33	{"wraps under zero", "-[>+<-]>++++++++++.", "\t"},
 34}
 35
 36func runLevel(t *testing.T, src string, lvl Level, input string) string {
 37	t.Helper()
 38	prog, err := Compile(src, lvl)
 39	uassert.NoError(t, err)
 40	if prog == nil {
 41		return ""
 42	}
 43	h := vmkit.NewTestHost().WithInput([]byte(input))
 44	m := NewMachine(prog)
 45	_, status := m.Step(h, vmkit.Unmetered)
 46	uassert.Equal(t, "halted", status.String())
 47	return h.OutString()
 48}
 49
 50// TestLadderLevelsAgree is the test the whole ladder rests on: an
 51// optimization that changes the output is not an optimization.
 52func TestLadderLevelsAgree(t *testing.T) {
 53	for _, tc := range corpus {
 54		naive := Execute(tc.src)
 55		uassert.Equal(t, tc.want, naive)
 56
 57		for _, lvl := range []Level{LevelJumps, LevelFuse, LevelIdioms} {
 58			got := runLevel(t, tc.src, lvl, "")
 59			if got != tc.want {
 60				t.Errorf("%s at level %s: got %q, want %q",
 61					tc.name, lvl.String(), got, tc.want)
 62			}
 63		}
 64	}
 65}
 66
 67func TestExecuteBaseline(t *testing.T) {
 68	// Rung 0 is kept verbatim, so this is also a regression test on the
 69	// thing the published cycles-per-op number was measured against.
 70	uassert.Equal(t, "Hello World", Execute(hello))
 71	uassert.Equal(t, 11, len(Execute(hello)))
 72}
 73
 74func TestCompileRejectsUnbalanced(t *testing.T) {
 75	cases := []string{"[", "]", "[[]", "+[->+<", "][", "[[[]]"}
 76	for _, src := range cases {
 77		_, err := CompileDefault(src)
 78		if err == nil {
 79			t.Errorf("Compile(%q) should have failed", src)
 80		}
 81	}
 82}
 83
 84func TestCompileRejectsOversizeSource(t *testing.T) {
 85	src := "+"
 86	for len(src) <= MaxSource {
 87		src += src // doubling, not O(n^2) appending
 88	}
 89	_, err := CompileDefault(src)
 90	uassert.ErrorIs(t, err, ErrSourceTooLong)
 91}
 92
 93func TestCompileRejectsUnknownLevel(t *testing.T) {
 94	_, err := Compile("+", Level(42))
 95	uassert.Error(t, err)
 96	_, err = Compile("+", Level(-1))
 97	uassert.Error(t, err)
 98}
 99
100// TestLadderShrinksTheOpStream is the compile-time half of the ladder: each
101// rung has to produce strictly fewer ops than the one below it, or it is not
102// buying anything.
103func TestLadderShrinksTheOpStream(t *testing.T) {
104	jumps, err := Compile(hello, LevelJumps)
105	uassert.NoError(t, err)
106	fuse, err := Compile(hello, LevelFuse)
107	uassert.NoError(t, err)
108	idioms, err := Compile(hello, LevelIdioms)
109	uassert.NoError(t, err)
110
111	uassert.True(t, jumps.Len() > fuse.Len())
112	uassert.True(t, fuse.Len() > idioms.Len())
113	uassert.Equal(t, "jumps", jumps.Level().String())
114	uassert.Equal(t, hello, idioms.Source())
115}
116
117func TestIdiomRecognition(t *testing.T) {
118	cases := []struct {
119		name string
120		src  string
121		want int // ops, excluding the trailing halt
122	}{
123		{"clear", "[-]", 1},                 // opSet
124		{"move", "[->+<]", 2},               // opAddMul, opSet
125		{"multiply two", "[->++>+++<<]", 3}, // two opAddMul, opSet
126		{"scan right", "[>]", 1},            // opScan
127		{"scan left", "[<]", 1},             // opScan
128	}
129	for _, tc := range cases {
130		prog, err := Compile(tc.src, LevelIdioms)
131		uassert.NoError(t, err)
132		if prog == nil {
133			continue
134		}
135		if got := prog.Len() - 1; got != tc.want {
136			t.Errorf("%s: %q compiled to %d ops, want %d",
137				tc.name, tc.src, got, tc.want)
138		}
139	}
140}
141
142// TestPlusLoopIsNotRewritten pins the conservative half of the rewrite: a
143// loop that adds one per iteration still reaches zero by wrapping, but only
144// after 256 iterations, and folding it would be a different program. It has
145// to stay a real loop.
146func TestPlusLoopIsNotRewritten(t *testing.T) {
147	prog, err := Compile("[+]", LevelIdioms)
148	uassert.NoError(t, err)
149	// opJmpZ, opAdd, opJmpNZ, opHalt: untouched.
150	uassert.Equal(t, 4, prog.Len())
151}
152
153func TestInputThroughTheHost(t *testing.T) {
154	// Read three bytes and echo them back.
155	const echo3 = ",.,.,."
156	h := vmkit.NewTestHost().WithInput([]byte("gno"))
157	m, err := Load(echo3)
158	uassert.NoError(t, err)
159	_, status := m.Step(h, vmkit.Unmetered)
160	uassert.Equal(t, "halted", status.String())
161	uassert.Equal(t, "gno", h.OutString())
162}
163
164func TestInputEndsAsZero(t *testing.T) {
165	// Past the end of input, `,` yields a zero cell. Without input at all,
166	// the naive interpreter would have panicked here.
167	h := vmkit.NewTestHost().WithInput([]byte("a"))
168	m, err := Load(",.+++++++++++++++++++++++++++++++++++++++++++++++.,+++++++++++++++++++++++++++++++++++++++++++++++++.")
169	uassert.NoError(t, err)
170	_, status := m.Step(h, vmkit.Unmetered)
171	uassert.Equal(t, "halted", status.String())
172	uassert.Equal(t, "a\x901", h.OutString())
173}
174
175func TestExecutePanicsOnInput(cur realm, t *testing.T) {
176	// Rung 0's second preserved bug, pinned so nobody "fixes" the baseline.
177	uassert.PanicsWithMessage(t, cur, "unsupported", func() { Execute(",") })
178}
179
180func TestMachineWithoutProgram(t *testing.T) {
181	m := &Machine{status: vmkit.Running}
182	used, status := m.Step(vmkit.NewTestHost(), 10)
183	uassert.Equal(t, int64(0), used)
184	uassert.Equal(t, "trapped", status.String())
185	uassert.Equal(t, "no program", m.Trap())
186}