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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bf_test.gno

6.29 Kb · 186 lines
  1package bf
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/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}