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

render.gno

6.31 Kb · 208 lines
  1package bfdemo
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/moul/kit/ui/v0"
  7	"gno.land/p/moul/x/vm/bf/v0"
  8	"gno.land/p/moul/x/vm/vmkit/v0"
  9	"gno.land/p/nt/ufmt/v0"
 10)
 11
 12// samples are the programs the home page offers, so the realm is usable
 13// without writing Brainfuck by hand.
 14var samples = []struct {
 15	name  string
 16	src   string
 17	input string
 18	note  string
 19}{
 20	{
 21		"Hello World",
 22		"++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.",
 23		"",
 24		"385 guest ops, five tape cells. Finishes in one slice.",
 25	},
 26	{
 27		"Echo the input",
 28		",[.,]",
 29		"gno.land",
 30		"Reads through Host.Input, which is what the naive interpreter could not do at all: it panicked on `,`.",
 31	},
 32	{
 33		"Heavy loop",
 34		"++++++++++[->++++++++++[->++++++++++[-]<]<]",
 35		"",
 36		"About 111k guest ops. Give it 20000 fuel and it takes six slices, which is the point.",
 37	},
 38}
 39
 40// Render is the realm's gnoweb view.
 41//
 42//   - "/"      the samples, the instance list, and how the thing works.
 43//   - "/<id>"  one instance: its program, status, fuel and output.
 44func Render(path string) string {
 45	id := strings.TrimPrefix(path, "/")
 46	id = strings.TrimSpace(id)
 47	if id == "" {
 48		return renderHome()
 49	}
 50	return renderInstance(id)
 51}
 52
 53func renderHome() string {
 54	var sb strings.Builder
 55	sb.WriteString("# Brainfuck, on chain, a slice at a time\n\n")
 56	sb.WriteString("Demo of [`p/moul/x/vm/bf`](/p/moul/x/vm/bf/v0) (the machine) and ")
 57	sb.WriteString("[`p/moul/x/vm/vmkit`](/p/moul/x/vm/vmkit/v0) (the host ABI, the fuel meter, ")
 58	sb.WriteString("the instance store). This realm holds no logic of its own.\n\n")
 59	sb.WriteString("A program here does not run to completion. It runs until its fuel slice is spent, ")
 60	sb.WriteString("then pauses: the realm keeps the snapshot and the next caller pays for the next slice. ")
 61	sb.WriteString("Realm code cannot pause itself, and a guest can.\n\n")
 62
 63	sb.WriteString("## Samples\n\n")
 64	for _, s := range samples {
 65		sb.WriteString("**" + ui.Inline(s.name) + "**: " + s.note + "\n\n")
 66		sb.WriteString("```\n" + s.src + "\n```\n\n")
 67		sb.WriteString(ui.Action("Upload "+s.name, "Upload",
 68			"src", s.src, "input", s.input, "budget", "0") + "\n\n")
 69	}
 70
 71	sb.WriteString("## Instances\n\n")
 72	t := ui.NewTable("id", "status", "fuel used", "slices", "output")
 73	store.ReverseIterate(func(i *vmkit.Instance) bool {
 74		t.Row(
 75			"["+i.ID+"](/r/moul/x/vm/bfdemo/v0:"+i.ID+")",
 76			i.Status.String(),
 77			ufmt.Sprintf("%d", i.FuelUsed),
 78			ufmt.Sprintf("%d", i.Slices),
 79			"`"+ui.Cell(printable(i.Output, 24))+"`",
 80		)
 81		return false
 82	})
 83	sb.WriteString(t.OrEmpty("No instances yet. Upload one of the samples above."))
 84	sb.WriteString("\n\n")
 85
 86	sb.WriteString("## Limits\n\n")
 87	lt := ui.NewTable("limit", "value")
 88	lt.Row("instances", ufmt.Sprintf("%d", MaxInstances))
 89	lt.Row("source bytes", ufmt.Sprintf("%d", bf.MaxSource))
 90	lt.Row("input bytes", ufmt.Sprintf("%d", MaxInput))
 91	lt.Row("output bytes", ufmt.Sprintf("%d", MaxOutput))
 92	lt.Row("fuel per slice", ufmt.Sprintf("%d", MaxSliceFuel))
 93	lt.Row("tape cells", ufmt.Sprintf("%d", bf.TapeSize))
 94	sb.WriteString(lt.String())
 95	return sb.String()
 96}
 97
 98func renderInstance(id string) string {
 99	var sb strings.Builder
100	sb.WriteString("# Instance " + ui.Inline(id) + "\n\n")
101	sb.WriteString("[← all instances](/r/moul/x/vm/bfdemo/v0)\n\n")
102
103	inst := store.Get(id)
104	if inst == nil {
105		sb.WriteString("**No such instance.** It was never uploaded, or its owner removed it.\n")
106		return sb.String()
107	}
108
109	t := ui.NewTable("field", "value")
110	t.Row("owner", ui.Addr(inst.Owner))
111	t.Row("vm", ui.Cell(inst.VM))
112	t.Row("status", inst.Status.String())
113	if inst.Trap != "" {
114		t.Row("trap", ui.Cell(inst.Trap))
115	}
116	t.Row("fuel used", ufmt.Sprintf("%d", inst.FuelUsed))
117	t.Row("fuel budget", fuelText(inst.FuelBudget))
118	t.Row("slices", ufmt.Sprintf("%d", inst.Slices))
119	t.Row("source bytes", ufmt.Sprintf("%d", len(inst.Program)))
120	t.Row("snapshot bytes", ufmt.Sprintf("%d", len(inst.Snapshot)))
121	sb.WriteString(t.String())
122	sb.WriteString("\n")
123
124	// The source is caller-supplied, and everything outside the eight
125	// operators is a comment that may hold anything at all. Show the
126	// program the machine actually compiled, which cannot contain a
127	// backtick or a newline and so cannot break out of the fence.
128	sb.WriteString("## Program\n\n```\n" + operatorsOnly(string(inst.Program)) + "\n```\n\n")
129
130	sb.WriteString("## Output\n\n")
131	if len(inst.Output) == 0 {
132		sb.WriteString(ui.Empty("Nothing written yet."))
133	} else {
134		sb.WriteString("```\n" + printable(inst.Output, MaxOutput) + "\n```\n")
135	}
136	sb.WriteString("\n")
137
138	if inst.Status == vmkit.Running {
139		sb.WriteString(ui.Action("Run 10000 more fuel", "Step", "id", id, "fuel", "10000"))
140		sb.WriteString(" · ")
141		sb.WriteString(ui.Action("Run 100000 more fuel", "Step", "id", id, "fuel", "100000"))
142		sb.WriteString("\n")
143	} else {
144		sb.WriteString("This instance is **" + inst.Status.String() + "** and cannot be stepped again.\n")
145	}
146	return sb.String()
147}
148
149func fuelText(n int64) string {
150	if n == vmkit.Unmetered {
151		return "unmetered"
152	}
153	return ufmt.Sprintf("%d", n)
154}
155
156// operatorsOnly strips a program down to the eight characters the language
157// defines. Everything else is a comment by definition, and a comment is
158// attacker-controlled text that has no business reaching a renderer.
159func operatorsOnly(src string) string {
160	var sb strings.Builder
161	for i := 0; i < len(src); i++ {
162		switch src[i] {
163		case '+', '-', '<', '>', '[', ']', '.', ',':
164			sb.WriteByte(src[i])
165		}
166	}
167	if sb.Len() == 0 {
168		return "(no operators: this program does nothing)"
169	}
170	return sb.String()
171}
172
173// printable renders guest output for a code fence: printable ASCII as itself,
174// everything else as an escape. A guest writes arbitrary bytes, so this is
175// the only form of it that is safe to show.
176func printable(b []byte, max int) string {
177	if len(b) == 0 {
178		return ""
179	}
180	truncated := false
181	if len(b) > max {
182		b, truncated = b[:max], true
183	}
184	const hexDigits = "0123456789abcdef"
185	var sb strings.Builder
186	for _, c := range b {
187		switch {
188		case c == '\n':
189			sb.WriteString("\\n")
190		case c == '\t':
191			sb.WriteString("\\t")
192		case c == '\\':
193			sb.WriteString("\\\\")
194		case c == '`':
195			sb.WriteString("\\x60")
196		case c >= 0x20 && c < 0x7f:
197			sb.WriteByte(c)
198		default:
199			sb.WriteString("\\x")
200			sb.WriteByte(hexDigits[c>>4])
201			sb.WriteByte(hexDigits[c&0x0f])
202		}
203	}
204	if truncated {
205		sb.WriteString("…")
206	}
207	return sb.String()
208}