From 71c1eab8188099583507a992554cb7ef4d096fd8 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 30 Sep 2024 19:42:28 -0400 Subject: [PATCH] Build initial plumbing for showing example programs --- packages/web/spec/program/example.mdx | 198 +++++++++++++ .../web/src/components/ProgramExample.tsx | 270 ++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 packages/web/spec/program/example.mdx create mode 100644 packages/web/src/components/ProgramExample.tsx diff --git a/packages/web/spec/program/example.mdx b/packages/web/spec/program/example.mdx new file mode 100644 index 00000000..f1378e77 --- /dev/null +++ b/packages/web/spec/program/example.mdx @@ -0,0 +1,198 @@ +--- +sidebar_position: 3 +--- + +import { ProgramExampleContextProvider, useProgramExampleContext, SourceContents, Opcodes } from "@site/src/components/ProgramExample"; + +# Example program + + ({ + code: { + source, + range: rangeFor("3 finney") + } + }) + }, + { + operation: { + mnemonic: "CALLVALUE" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("msg.callvalue") + } + }) + }, + { + operation: { + mnemonic: "LT" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("msg.callvalue < 3 finney") + } + }) + }, + { + operation: { + mnemonic: "PUSH1", + arguments: ["0x13"] + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("if (msg.callvalue < 3 finney) {\n return;\n }") + } + }) + }, + { + operation: { + mnemonic: "JUMPI" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("if (msg.callvalue < 3 finney) {\n return;\n }") + } + }) + }, + { + operation: { + mnemonic: "PUSH0", + comment: "begin paid incrementing" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("storedValue") + } + }) + }, + { + operation: { + mnemonic: "SLOAD" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("storedValue") + } + }) + }, + { + operation: { + mnemonic: "PUSH1", + arguments: ["0x01"] + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("1") + } + }) + }, + { + operation: { + mnemonic: "ADD" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("localValue = localValue + 1;") + } + }) + }, + { + operation: { + mnemonic: "PUSH0" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("storedValue = localValue;") + } + }) + }, + { + operation: { + mnemonic: "SSTORE" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("storedValue = localValue;") + } + }) + }, + { + operation: { + mnemonic: "JUMPDEST", + comment: "skip to here if not enough paid" + }, + context: ({ source, rangeFor }) => ({ + code: { + source, + range: rangeFor("return;") + } + }) + } + ]} +> + + +This page helps illustrate this program schema's +[key concepts](/spec/program/concepts) by offering a fictional +pseudo-code example and its hypothetical compiled program. + + +## Source code + +Assume this fictional high-level language expects exactly one contract per +source file. The following code might be used to define a contract that +increments a state variable if the caller pays at least 1 finney (0.001 +ETH). + + + +This contract's storage layout has exactly one record: a `uint256` value +named `storedValue` at slot `0`. + +## Compiled opcodes + +Because fictional optimizers have no theoretical limits, assume the source +contract above produced bytecode equivalent to the following brief sequence of +operations, listed by their offsets: + + + + +## Program example + + diff --git a/packages/web/src/components/ProgramExample.tsx b/packages/web/src/components/ProgramExample.tsx new file mode 100644 index 00000000..331f8076 --- /dev/null +++ b/packages/web/src/components/ProgramExample.tsx @@ -0,0 +1,270 @@ +import React, { createContext, useContext, useState, useEffect } from "react"; + +import CodeBlock, { type Props as CodeBlockProps } from "@theme/CodeBlock"; + +export interface ProgramExampleProps { + sourcePath: string; + sourceContents: string; + instructions: Instruction[]; +} + +interface Instruction { + operation: Operation; + context: Context | ContextThunk; +} + +interface InstructionWithOffset extends Instruction { + offset: number; +} + +interface Operation { + mnemonic: string; + arguments?: `0x${string}`[]; + comment?: string; +} + +export type ContextThunk = (props: { + source: { id: any; }; + rangeFor(query: string): { + offset: number; + length: number; + }; +}) => Context; + +export interface Context { + code?: { + source: { + id: any; + }; + range?: { + offset: number; + length: number; + } + }; +} + +interface ProgramExampleContextValue { + // props + sourcePath: string; + sourceContents: string; + instructions: (Instruction & { offset: number })[]; + + // stateful stuff + context: Context | undefined; + highlightInstruction(offset: number | undefined): void; +} + +const ProgramExampleContext = + createContext(undefined); + +export function useProgramExampleContext() { + const context = useContext(ProgramExampleContext); + if (context === undefined) { + throw new Error("useProgramExampleContext must be used within a ProgramExampleContextProvider"); + } + + return context; +} + +export function ProgramExampleContextProvider({ + children, + ...props +}: ProgramExampleProps & { + children: React.ReactNode; +}): JSX.Element { + const { + sourcePath, + sourceContents, + instructions: instructionsWithoutOffsets + } = props; + + const instructions = computeOffsets(instructionsWithoutOffsets); + + const [ + highlightedOffset, + highlightInstruction + ] = useState(); + + const [context, setContext] = useState(); + + useEffect(() => { + if (typeof highlightedOffset === "undefined") { + setContext(undefined); + return; + } + + const instruction = instructions + .find(({ offset }) => offset === highlightedOffset); + + if (!instruction) { + throw new Error(`Unexpected could not find instruction with offset ${highlightedOffset}`); + } + + const context = resolveContext(instruction.context, props); + + setContext(context); + }, [highlightedOffset, setContext]); + + return + {children} + +} + +function resolveContext( + context: Context | ContextThunk, + props: ProgramExampleProps +): Context { + if (typeof context !== "function") { + return context; + } + + const { sourcePath, sourceContents } = props; + + const source = { + id: sourcePath + }; + + const rangeFor = (query: string) => { + const offset = sourceContents.indexOf(query); + if (offset === -1) { + throw new Error(`Unexpected could not find string ${query}`); + } + + const length = query.length; + + return { + offset, + length + }; + }; + + return context({ source, rangeFor }); +} + +export function SourceContents( + props: Exclude +): JSX.Element { + const { + sourceContents, + + context + } = useProgramExampleContext(); + + useEffect(() => { + console.log("context %o", context); + }, [context]); + + return { + sourceContents + }; +} + +export function Opcodes(): JSX.Element { + const { + instructions, + highlightInstruction + } = useProgramExampleContext(); + + const paddingLength = instructions.at(-1)!.offset.toString(16).length; + + return
{ + instructions.map(({ offset, operation }) => + highlightInstruction(offset)} + onMouseLeave={() => highlightInstruction(undefined)} + /> + ) + }
+} + + +function Opcode(props: { + offset: number; + paddingLength: number; + operation: Operation; + onMouseEnter: () => void; + onMouseLeave: () => void; +}): JSX.Element { + const { + offset, + paddingLength, + operation, + onMouseEnter, + onMouseLeave + } = props; + + const offsetLabel = <> + 0x{offset.toString(16).padStart(paddingLength, "0")} + ; + + const operationLabel = { + [operation.mnemonic, ...operation.arguments || []].join(" ") + }; + + return <> +
{ + offsetLabel + }
+
{ + operationLabel + }
+ ; +} + +function computeOffsets(instructions: Instruction[]): InstructionWithOffset[] { + const initialResults: { + nextOffset: number; + results: InstructionWithOffset[]; + } = { + nextOffset: 0, + results: [] + }; + + const { + results + } = instructions.reduce( + ({ nextOffset, results }, instruction) => { + const result = { + ...instruction, + offset: nextOffset + }; + + const operationSize = ( + 1 /* for opcode */ + + Math.ceil( + (instruction.operation.arguments || []) + .map(prefixed => prefixed.slice(2)) + .join("") + .length / 2 + ) + ); + + return { + nextOffset: nextOffset + operationSize, + results: [ + ...results, + result + ] + }; + }, + initialResults + ); + + return results; +}