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}