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

compile.gno

9.18 Kb · 327 lines
  1package bf
  2
  3import (
  4	"errors"
  5
  6	"gno.land/p/nt/ufmt/v0"
  7)
  8
  9// TapeSize is the tape length of a compiled [Machine]. It matches
 10// [NaiveTapeSize] so that the ladder compares like with like: a rung that
 11// changed the semantics would not be a rung, it would be a different program.
 12const TapeSize = 30000
 13
 14// MaxSource bounds the program a realm will accept. Compilation is linear in
 15// the source, but the op array and every snapshot of it are consensus state,
 16// so the size has to be bounded somewhere the caller can see.
 17const MaxSource = 64 * 1024
 18
 19// ErrSourceTooLong is returned by [Compile] for a program above [MaxSource].
 20var ErrSourceTooLong = errors.New("bf: source too long")
 21
 22// Level selects how far up the optimization ladder [Compile] goes.
 23//
 24// Every level produces the same output for the same program: the levels exist
 25// so the cost of each optimization can be measured in isolation, which is the
 26// published result this package is really for. Rung 0 of the ladder is
 27// [Execute] and has no compiler at all.
 28type Level int
 29
 30const (
 31	// LevelJumps is rung 1: one op per source byte, with the jump targets
 32	// resolved at compile time. This is the rung that kills the brace
 33	// rescan, which in [Execute] costs O(loop body) on every iteration.
 34	LevelJumps Level = iota
 35
 36	// LevelFuse is rung 2: runs of identical operators collapse, so
 37	// "+++++" is one add-5 and ">>>" is one move-3. Real programs are
 38	// mostly runs.
 39	LevelFuse
 40
 41	// LevelIdioms is rung 3: a loop whose body only moves and adds, and
 42	// which returns to where it started while decrementing the current
 43	// cell by one, is replaced by the multiply-adds it performs plus a
 44	// clear. "[-]" becomes one op, "[->+<]" becomes two.
 45	LevelIdioms
 46)
 47
 48// Default is the level [CompileDefault] uses, and the one a realm should
 49// want: every optimization this package knows, same semantics.
 50const Default = LevelIdioms
 51
 52// String names the level, for the ladder table and for realm output.
 53func (l Level) String() string {
 54	switch l {
 55	case LevelJumps:
 56		return "jumps"
 57	case LevelFuse:
 58		return "fuse"
 59	case LevelIdioms:
 60		return "idioms"
 61	}
 62	return "unknown"
 63}
 64
 65type opcode byte
 66
 67const (
 68	opAdd    opcode = iota // tape[p] += arg           (arg normalized to 0..255)
 69	opMove                 // p += arg
 70	opOut                  // write tape[p] to the host
 71	opIn                   // read one input byte into tape[p]
 72	opJmpZ                 // if tape[p] == 0 { pc = arg }
 73	opJmpNZ                // if tape[p] != 0 { pc = arg }
 74	opSet                  // tape[p] = byte(arg)
 75	opAddMul               // tape[p+off] += tape[p] * byte(arg)
 76	opScan                 // while tape[p] != 0 { p += arg }
 77	opHalt                 // end of program
 78)
 79
 80// op is one compiled instruction. It is a flat struct on purpose: the
 81// execution loop indexes an array of these, and an op that owned a slice
 82// would allocate in the hot path.
 83type op struct {
 84	code opcode
 85	arg  int
 86	off  int
 87}
 88
 89// Program is compiled Brainfuck, ready to run on a [Machine].
 90type Program struct {
 91	ops   []op
 92	src   string
 93	level Level
 94}
 95
 96// Source returns the program text the [Program] was compiled from.
 97func (p *Program) Source() string { return p.src }
 98
 99// Level returns the ladder rung this program was compiled at.
100func (p *Program) Level() Level { return p.level }
101
102// Len returns the number of compiled ops, including the trailing halt. It is
103// the number worth quoting next to the source length: the ratio is exactly
104// what the ladder buys.
105func (p *Program) Len() int { return len(p.ops) }
106
107// CompileDefault compiles at [Default].
108func CompileDefault(src string) (*Program, error) { return Compile(src, Default) }
109
110// Compile turns Brainfuck source into a [Program] at the requested ladder
111// level. Everything that is not one of the eight operators is a comment, as
112// the language requires.
113//
114// It fails on unbalanced brackets rather than trusting the runtime to notice,
115// which is the first thing that separates it from [Execute]: a malformed
116// program is rejected before anybody pays to run it.
117func Compile(src string, lvl Level) (*Program, error) {
118	if len(src) > MaxSource {
119		return nil, ErrSourceTooLong
120	}
121	if lvl < LevelJumps || lvl > LevelIdioms {
122		return nil, ufmt.Errorf("bf: unknown level %d", int(lvl))
123	}
124
125	ops := []op{}
126	opens := []int{} // pc of each unclosed opJmpZ
127
128	for i := 0; i < len(src); i++ {
129		c := src[i]
130		switch c {
131		case '+', '-':
132			delta := 1
133			if c == '-' {
134				delta = -1
135			}
136			if lvl >= LevelFuse {
137				n := 1
138				for i+n < len(src) && (src[i+n] == '+' || src[i+n] == '-') {
139					if src[i+n] == '+' {
140						delta++
141					} else {
142						delta--
143					}
144					n++
145				}
146				i += n - 1
147			}
148			if a := normAdd(delta); a != 0 {
149				ops = append(ops, op{code: opAdd, arg: a})
150			}
151		case '>', '<':
152			delta := 1
153			if c == '<' {
154				delta = -1
155			}
156			if lvl >= LevelFuse {
157				n := 1
158				for i+n < len(src) && (src[i+n] == '>' || src[i+n] == '<') {
159					if src[i+n] == '>' {
160						delta++
161					} else {
162						delta--
163					}
164					n++
165				}
166				i += n - 1
167			}
168			if d := normMove(delta); d != 0 {
169				ops = append(ops, op{code: opMove, arg: d})
170			}
171		case '.':
172			ops = append(ops, op{code: opOut})
173		case ',':
174			ops = append(ops, op{code: opIn})
175		case '[':
176			opens = append(opens, len(ops))
177			ops = append(ops, op{code: opJmpZ})
178		case ']':
179			if len(opens) == 0 {
180				return nil, ufmt.Errorf("bf: unmatched ']' at byte %d", i)
181			}
182			open := opens[len(opens)-1]
183			opens = opens[:len(opens)-1]
184
185			if lvl >= LevelIdioms {
186				if idiom, ok := simpleLoop(ops[open+1:]); ok {
187					ops = append(ops[:open], idiom...)
188					continue
189				}
190			}
191			// pc lands on the op after the jump, so a jump target is
192			// the index of the partner op itself.
193			ops = append(ops, op{code: opJmpNZ, arg: open})
194			ops[open].arg = len(ops) - 1
195		}
196	}
197	if len(opens) != 0 {
198		return nil, ufmt.Errorf("bf: %d unmatched '['", len(opens))
199	}
200
201	ops = append(ops, op{code: opHalt})
202	return &Program{ops: ops, src: src, level: lvl}, nil
203}
204
205// normAdd folds a signed delta into the 0..255 the byte tape actually sees,
206// so the execution loop never has to convert a negative int to a byte.
207func normAdd(d int) int {
208	d %= 256
209	if d < 0 {
210		d += 256
211	}
212	return d
213}
214
215// normMove folds a pointer delta into one lap of the tape. Moving right
216// TapeSize times is a no-op on a wrapping tape, in the naive interpreter as
217// much as here, so collapsing it changes nothing but the op count.
218func normMove(d int) int {
219	d %= TapeSize
220	if d < 0 {
221		d += TapeSize
222	}
223	if d > TapeSize/2 {
224		d -= TapeSize
225	}
226	return d
227}
228
229// cell is one offset the body of a simple loop writes to.
230type cell struct {
231	off   int
232	delta int
233}
234
235// simpleLoop decides whether a loop body can be replaced by straight-line
236// code, and returns that code.
237//
238// Two shapes qualify, and they are the two that dominate real programs:
239//
240//   - The body only moves, and ends somewhere other than where it started:
241//     that is a scan, "[>]" walking to the next zero cell.
242//   - The body only moves and adds, ends where it started, and takes exactly
243//     one off the current cell: that is a multiply-add. The loop runs
244//     tape[p] times, so every other cell it touches gains its delta times
245//     tape[p], and the current cell ends at zero. "[-]" is the degenerate
246//     case with no other cells.
247//
248// Anything else, including any body containing I/O or a nested loop that was
249// itself rewritten, is left alone. Being conservative here costs a few ops in
250// rare programs and is the only reason this rewrite is safe at all: a loop
251// whose current cell does not reach zero in steps of one is not guaranteed to
252// terminate, and constant-folding it would change the program.
253func simpleLoop(body []op) ([]op, bool) {
254	cursor := 0
255	cells := []cell{}
256	adds := 0
257
258	for _, o := range body {
259		switch o.code {
260		case opMove:
261			cursor += o.arg
262		case opAdd:
263			adds++
264			// Carry the delta as a signed value; it is normalized
265			// back into 0..255 when it is emitted.
266			d := o.arg
267			if d > 128 {
268				d -= 256
269			}
270			cells = addCell(cells, cursor, d)
271		default:
272			return nil, false
273		}
274	}
275
276	if cursor != 0 {
277		// Net movement: only a pure scan qualifies, and only when the
278		// step is not zero (which normMove already guarantees here).
279		if adds != 0 {
280			return nil, false
281		}
282		return []op{{code: opScan, arg: cursor}}, true
283	}
284
285	// Balanced. The current cell must fall by exactly one per iteration,
286	// or the loop is not a counted multiply-add.
287	if delta(cells, 0) != -1 {
288		return nil, false
289	}
290
291	out := []op{}
292	for _, c := range cells {
293		if c.off == 0 || c.delta == 0 {
294			continue
295		}
296		out = append(out, op{code: opAddMul, arg: normAdd(c.delta), off: c.off})
297	}
298	out = append(out, op{code: opSet, arg: 0})
299	return out, true
300}
301
302// addCell accumulates a delta at an offset, keeping cells sorted by offset so
303// that the op stream a program compiles to is identical on every node.
304func addCell(cells []cell, off, d int) []cell {
305	for i := range cells {
306		if cells[i].off == off {
307			cells[i].delta += d
308			return cells
309		}
310		if cells[i].off > off {
311			cells = append(cells, cell{})
312			copy(cells[i+1:], cells[i:])
313			cells[i] = cell{off: off, delta: d}
314			return cells
315		}
316	}
317	return append(cells, cell{off: off, delta: d})
318}
319
320func delta(cells []cell, off int) int {
321	for _, c := range cells {
322		if c.off == off {
323			return c.delta
324		}
325	}
326	return 0
327}