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