render.gno
7.80 Kb · 244 lines
1package riscvdemo
2
3import (
4 "strings"
5
6 "gno.land/p/moul/kit/ui/v0"
7 "gno.land/p/moul/x/vm/riscv/v0"
8 "gno.land/p/moul/x/vm/vmkit/v0"
9 "gno.land/p/nt/ufmt/v0"
10)
11
12// regNames are the ABI names, because nobody reading a register dump thinks in
13// x-numbers and every compiler's output is annotated with these.
14var regNames = [32]string{
15 "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2",
16 "s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5",
17 "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7",
18 "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6",
19}
20
21// Render is the realm's gnoweb view.
22//
23// - "/" the samples, the instance list, and what the numbers mean.
24// - "/<id>" one instance: its status, fuel, output and register file.
25func Render(path string) string {
26 id := strings.TrimSpace(strings.TrimPrefix(path, "/"))
27 if id == "" {
28 return renderHome()
29 }
30 return renderInstance(id)
31}
32
33func renderHome() string {
34 var sb strings.Builder
35 sb.WriteString("# RV32IM, on chain, a slice at a time\n\n")
36 sb.WriteString("Demo of [`p/moul/x/vm/riscv`](/p/moul/x/vm/riscv/v0) (the hart) and ")
37 sb.WriteString("[`p/moul/x/vm/vmkit`](/p/moul/x/vm/vmkit/v0) (the host ABI, the fuel meter, ")
38 sb.WriteString("the instance store). This realm holds no logic of its own.\n\n")
39 sb.WriteString("The programs below are flat `.text` images: the same bytes a cross compiler ")
40 sb.WriteString("emits for `riscv32im-unknown-none-elf`. Nothing here was written in a ")
41 sb.WriteString("chain language, and nothing translated it.\n\n")
42 sb.WriteString("A program does not run to completion. It runs until its fuel slice is spent, ")
43 sb.WriteString("then pauses: the realm keeps the snapshot and the next caller pays for the ")
44 sb.WriteString("next slice. Realm code cannot pause itself, and a guest can.\n\n")
45
46 sb.WriteString("## Samples\n\n")
47 for i := range samples {
48 s := &samples[i]
49 sb.WriteString("**" + ui.Inline(s.name) + "**: " + s.note + "\n\n")
50 sb.WriteString("```" + s.lang() + "\n" + s.asm + "\n```\n\n")
51 sb.WriteString(ui.Action("Upload "+s.name, "UploadSample",
52 "name", s.slug, "input", s.input,
53 "budget", ufmt.Sprintf("%d", s.budget)) + "\n\n")
54 }
55
56 sb.WriteString("## Instances\n\n")
57 t := ui.NewTable("id", "status", "instructions", "slices", "output")
58 store.ReverseIterate(func(i *vmkit.Instance) bool {
59 t.Row(
60 "["+i.ID+"](/r/moul/x/vm/riscvdemo/v0:"+i.ID+")",
61 i.Status.String(),
62 ufmt.Sprintf("%d", i.FuelUsed),
63 ufmt.Sprintf("%d", i.Slices),
64 "`"+ui.Cell(printable(i.Output, 24))+"`",
65 )
66 return false
67 })
68 sb.WriteString(t.OrEmpty("No instances yet. Upload one of the samples above."))
69 sb.WriteString("\n\n")
70
71 sb.WriteString("## What a slice costs\n\n")
72 sb.WriteString("An RV32IM instruction costs about **8,500 gas** here, measured rather than ")
73 sb.WriteString("estimated, so a block buys roughly **350,000 guest instructions**. Loading ")
74 sb.WriteString("the image costs about **19,600 gas per instruction word** and is paid again ")
75 sb.WriteString("on every resume, which is why the image cap below is small. The numbers and ")
76 sb.WriteString("how they were taken are in the [library README](/p/moul/x/vm/riscv/v0).\n\n")
77
78 sb.WriteString("## Limits\n\n")
79 lt := ui.NewTable("limit", "value")
80 lt.Row("instances", ufmt.Sprintf("%d", MaxInstances))
81 lt.Row("image bytes", ufmt.Sprintf("%d", MaxImage))
82 lt.Row("input bytes", ufmt.Sprintf("%d", MaxInput))
83 lt.Row("output bytes", ufmt.Sprintf("%d", MaxOutput))
84 lt.Row("instructions per slice", ufmt.Sprintf("%d", MaxSliceFuel))
85 lt.Row("address space", ufmt.Sprintf("%d", riscv.MemSize))
86 sb.WriteString(lt.String())
87 return sb.String()
88}
89
90func renderInstance(id string) string {
91 var sb strings.Builder
92 sb.WriteString("# Instance " + ui.Inline(id) + "\n\n")
93 sb.WriteString("[← all instances](/r/moul/x/vm/riscvdemo/v0)\n\n")
94
95 inst := store.Get(id)
96 if inst == nil {
97 sb.WriteString("**No such instance.** It was never uploaded, or its owner removed it.\n")
98 return sb.String()
99 }
100
101 t := ui.NewTable("field", "value")
102 t.Row("owner", ui.Addr(inst.Owner))
103 t.Row("vm", ui.Cell(inst.VM))
104 t.Row("status", inst.Status.String())
105 if inst.Trap != "" {
106 t.Row("trap", ui.Cell(inst.Trap))
107 }
108 t.Row("instructions run", ufmt.Sprintf("%d", inst.FuelUsed))
109 t.Row("budget", fuelText(inst.FuelBudget))
110 t.Row("slices", ufmt.Sprintf("%d", inst.Slices))
111 t.Row("image bytes", ufmt.Sprintf("%d", len(inst.Program)))
112 t.Row("image words", ufmt.Sprintf("%d", len(inst.Program)/4))
113 t.Row("snapshot bytes", ufmt.Sprintf("%d", len(inst.Snapshot)))
114 sb.WriteString(t.String())
115 sb.WriteString("\n")
116
117 sb.WriteString("## Registers\n\n")
118 sb.WriteString(renderRegisters(inst))
119
120 sb.WriteString("## Output\n\n")
121 if len(inst.Output) == 0 {
122 sb.WriteString(ui.Empty("Nothing written yet."))
123 } else {
124 sb.WriteString("```\n" + printable(inst.Output, MaxOutput) + "\n```\n")
125 }
126 sb.WriteString("\n")
127
128 sb.WriteString("## Image\n\n```\n" + hexDump(inst.Program, 64) + "\n```\n\n")
129
130 if inst.Status == vmkit.Running {
131 sb.WriteString(ui.Action("Run 25000 instructions", "Step", "id", id, "fuel", "25000"))
132 sb.WriteString(" · ")
133 sb.WriteString(ui.Action("Run 100000 instructions", "Step", "id", id, "fuel", "100000"))
134 sb.WriteString("\n")
135 } else {
136 sb.WriteString("This instance is **" + inst.Status.String() + "** and cannot be stepped again.\n")
137 }
138 return sb.String()
139}
140
141// renderRegisters restores the snapshot to read the register file out of it.
142// The hart is not kept between transactions, only its snapshot is, so this is
143// the only place the state is legible, and it is the thing worth seeing: a
144// program that wrote nothing still computed something.
145func renderRegisters(inst *vmkit.Instance) string {
146 if len(inst.Snapshot) == 0 {
147 return ui.Empty("Not started yet: no snapshot to read.") + "\n\n"
148 }
149 var m riscv.Machine
150 if err := m.Restore(inst.Snapshot); err != nil {
151 return ui.Empty("The snapshot could not be read.") + "\n\n"
152 }
153 reg := m.Registers()
154
155 var sb strings.Builder
156 t := ui.NewTable("register", "hex", "unsigned", "signed")
157 t.Row("pc", ufmt.Sprintf("0x%08X", m.PC()), "", "")
158 shown := 0
159 for i := 1; i < 32; i++ {
160 if reg[i] == 0 {
161 continue
162 }
163 t.Row(
164 regNames[i],
165 ufmt.Sprintf("0x%08X", reg[i]),
166 ufmt.Sprintf("%d", reg[i]),
167 ufmt.Sprintf("%d", int64(int32(reg[i]))),
168 )
169 shown++
170 }
171 sb.WriteString(t.String())
172 if shown == 0 {
173 sb.WriteString("\nEvery register except `pc` is zero.\n")
174 }
175 sb.WriteString("\n")
176 return sb.String()
177}
178
179func fuelText(n int64) string {
180 if n == vmkit.Unmetered {
181 return "unmetered"
182 }
183 return ufmt.Sprintf("%d", n)
184}
185
186const hexDigits = "0123456789abcdef"
187
188// hexDump shows the image as words, which is how anyone reading RV32 reads it.
189func hexDump(b []byte, max int) string {
190 truncated := false
191 if len(b) > max {
192 b, truncated = b[:max], true
193 }
194 var sb strings.Builder
195 for i := 0; i+3 < len(b); i += 4 {
196 if i > 0 && i%16 == 0 {
197 sb.WriteString("\n")
198 } else if i > 0 {
199 sb.WriteString(" ")
200 }
201 w := uint32(b[i]) | uint32(b[i+1])<<8 | uint32(b[i+2])<<16 | uint32(b[i+3])<<24
202 sb.WriteString(ufmt.Sprintf("%08x", w))
203 }
204 if truncated {
205 sb.WriteString("\n...")
206 }
207 return sb.String()
208}
209
210// printable renders guest output for a code fence: printable ASCII as itself,
211// everything else as an escape. A guest writes arbitrary bytes, so this is the
212// only form of it that is safe to show.
213func printable(b []byte, max int) string {
214 if len(b) == 0 {
215 return ""
216 }
217 truncated := false
218 if len(b) > max {
219 b, truncated = b[:max], true
220 }
221 var sb strings.Builder
222 for _, c := range b {
223 switch {
224 case c == '\n':
225 sb.WriteString("\\n")
226 case c == '\t':
227 sb.WriteString("\\t")
228 case c == '\\':
229 sb.WriteString("\\\\")
230 case c == '`':
231 sb.WriteString("\\x60")
232 case c >= 0x20 && c < 0x7f:
233 sb.WriteByte(c)
234 default:
235 sb.WriteString("\\x")
236 sb.WriteByte(hexDigits[c>>4])
237 sb.WriteByte(hexDigits[c&0x0f])
238 }
239 }
240 if truncated {
241 sb.WriteString("…")
242 }
243 return sb.String()
244}