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

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}