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}