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

render.gno

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