From 03b4298d11d25d23211351bf67d95ec1e1c1508c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 21 Dec 2023 23:17:57 -0500 Subject: [PATCH] Specify ethdebug/format/type/base ... to serve as a minimally-necessary schema for all type representations, to be extended by more-specific subschemas for known kinds of types. Specifically: - Define formal JSON Schema in YAML - Add "Type schemas" section to specification site - Include overview page to describe how type schemas are organized - Include placeholder page for to-be-written canonical type schema - Add page to Type schemas section to informally specify ethdebug/format/type/base - Introduce purpose of this schema and how it fits more broadly - Explain key concepts defined by this schema - Represent full base schema via interactive nested React components - Provide example (non-canonical) schema extensions for a few types --- schemas/type/base.schema.yaml | 128 +++++++ web/docusaurus.config.ts | 23 ++ web/spec/{schema => type}/_category_.json | 2 +- web/spec/type/base.mdx | 400 ++++++++++++++++++++++ web/spec/type/overview.mdx | 98 ++++++ web/spec/type/type.mdx | 21 ++ web/src/components/SchemaViewer.tsx | 85 ++++- web/src/loadSchema.ts | 4 + 8 files changed, 754 insertions(+), 7 deletions(-) create mode 100644 schemas/type/base.schema.yaml rename web/spec/{schema => type}/_category_.json (83%) create mode 100644 web/spec/type/base.mdx create mode 100644 web/spec/type/overview.mdx create mode 100644 web/spec/type/type.mdx diff --git a/schemas/type/base.schema.yaml b/schemas/type/base.schema.yaml new file mode 100644 index 00000000..88923c86 --- /dev/null +++ b/schemas/type/base.schema.yaml @@ -0,0 +1,128 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/type/base" + +title: ethdebug/format/type/base +description: + Defines the minimally necessary schema for a data type. + Types belong to a particular `class` (`"elementary"` or `"complex"`), + and are further identified by a particular `kind`. +type: object +oneOf: + - $ref: "#/$defs/ElementaryType" + - $ref: "#/$defs/ComplexType" + +$defs: + ElementaryType: + title: ElementaryType + description: + Represents an elementary type (one that does not compose other types) + type: object + properties: + class: + type: string + const: elementary + kind: + type: string + contains: + not: + description: + "**Elementary types must not specify a `contains` field + (to make it easier to discriminate elementary vs. complex)**" + required: + - kind + examples: + - kind: uint + bits: 256 + + ComplexType: + title: ComplexType + description: + Represents a complex type, one that composes other types (e.g., arrays, + structs, mappings) + type: object + properties: + class: + type: string + const: complex + description: Indicates that this is a complex type + kind: + type: string + description: The specific kind of complex type, e.g., array or struct + contains: + title: ComplexType.contains + oneOf: + - $ref: "#/$defs/TypeWrapper" + - $ref: "#/$defs/TypeWrapperArray" + - $ref: "#/$defs/TypeWrapperObject" + required: + - kind + - contains + examples: + - kind: array + contains: + type: + kind: uint + bits: 256 + - kind: struct + contains: + - member: x + type: + kind: uint + bits: 256 + - member: y + type: + kind: uint + bits: 256 + - kind: mapping + contains: + key: + type: + kind: address + payable: true + value: + type: + kind: uint + bits: 256 + + TypeReference: + title: '{ "id": ... }' + description: A reference to a known type by ID + type: object + properties: + id: + type: + - string + - number + additionalProperties: false + required: + - id + + TypeWrapper: + title: '{ "type": ... }' + description: + A wrapper around a type. Defines a `"type"` field that may include a full + Type representation or a reference to a known Type by ID. Note that this + schema permits additional properties on the same object. + type: object + properties: + type: + oneOf: + - $ref: "schema:ethdebug/format/type/base" + - $ref: "#/$defs/TypeReference" + required: + - type + + TypeWrapperArray: + title: '{ "type": ... }[]' + description: A list of wrapped types, where the wrapper may add fields + type: array + items: + $ref: "#/$defs/TypeWrapper" + + TypeWrapperObject: + title: '{ "key": { "type": ... }, ... }' + description: + A key-value mapping of wrapped types, where the wrapper may add fields + type: object + additionalProperties: + $ref: "#/$defs/TypeWrapper" diff --git a/web/docusaurus.config.ts b/web/docusaurus.config.ts index 92a6eb20..0c4b127b 100644 --- a/web/docusaurus.config.ts +++ b/web/docusaurus.config.ts @@ -1,4 +1,5 @@ import {themes as prismThemes} from 'prism-react-renderer'; +import path from "path"; import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; import type { Configuration } from "webpack"; @@ -53,6 +54,25 @@ const config: Config = { }; }, + async function ignoreBuffer(context, options) { + return { + name: "ignore-buffer", + configureWebpack(config: Configuration) { + return { + resolve: { + alias: { + react: path.resolve('./node_modules/react'), + }, + fallback: { + buffer: false + } + } + }; + } + } + + }, + // Used to maintain separate spec/ directory, outside the core docs/ [ '@docusaurus/plugin-content-docs', @@ -157,6 +177,9 @@ const config: Config = { prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula, + additionalLanguages: [ + "json" + ], }, } satisfies Preset.ThemeConfig, }; diff --git a/web/spec/schema/_category_.json b/web/spec/type/_category_.json similarity index 83% rename from web/spec/schema/_category_.json rename to web/spec/type/_category_.json index facdbe72..d3684fa3 100644 --- a/web/spec/schema/_category_.json +++ b/web/spec/type/_category_.json @@ -1,5 +1,5 @@ { - "label": "Schema", + "label": "Type schemas", "position": 3, "link": { "type": "generated-index", diff --git a/web/spec/type/base.mdx b/web/spec/type/base.mdx new file mode 100644 index 00000000..50fb6272 --- /dev/null +++ b/web/spec/type/base.mdx @@ -0,0 +1,400 @@ +--- +sidebar_position: 3 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; +import TOCInline from '@theme/TOCInline'; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import yaml from "yaml-template"; + +# ethdebug/format/type/base + +:::warning +The schema on this page is extended by other, more specific schemas as part of +the larger **ethdebug/format** specification. These other schemas specify the +representation of many common kinds of types (e.g. including signed/unsigned +integers, arrays, structs, mappings, etc.). In order to adhere to this format +fully, compilers that represent known types **should** do so with the +appropriate more-specific schema. + +Please see the [**ethdebug/format/type**](/spec/type) schema for representing +these supported types. +::: + +This format defines this base schema (**ethdebug/format/type/base**) for +representing data types from high-level languages. These types may be +user-defined or supplied as native data types in a language. This schema +affords the representation of complex/parametric types, whose definition +composes other types (e.g., arrays and structs, which contain at least one +underlying type). + +This base schema itself is designed to be extended by other schemas in this +format. It serves to specify what is _minimally necessary_ for a type to be +a valid representation (i.e., all type representations **must** adhere to at +least this base schema). + + + +## Key concepts + +The **ethdebug/format/type/base** schema includes definitions for a few +concepts that are worth highlighting here. + +### Types are organized by `kind` + +:::info[Example: Boolean type] +```json +{ + "kind": "bool" +} +``` +::: + +An **ethdebug/format/type/base** type representation is a JSON object with a +`kind` field containing a string value. + +This field is intended for use by other **ethdebug/format** schemas, which +reserve specific string values for the various `kind`s of types that +this format supports, and for use by implementers of compilers and debuggers +in situations where coordinating outside this specification is necessary or +beneficial. + +`kind` is a required field for all type representations. + +The primary purpose for the `kind` field is to discriminate type objects into +the appropriate corresponding subschema for a well-understood family of type. +Although **ethdebug/format/type/base** does not impose any constraints on +objects based on the `kind` field, it includes this field so as to encourage +the one-to-one pairing between values for this field and corresponding +subschemas. + +In other words: when extending this schema, ensure there exists exactly one +corresponding subschema for each defined value of `kind`. + +### Elementary vs. complex types + +Type representations in this schema fall into one of two `class`es: either +`"elementary"` or `"complex"`. Type representations express this disinction in +two ways (the optional `"class"` field, and the absence or existence of a +`"contains"` field). + +- Elementary types do not compose any other types. For example, `uint256` is an + elementary type. `string` may be an elementary type for languages that whose + semantics treat strings differently than simply an array of characters (like + Solidity does). + +- Complex types compose at least one other type. For instance, `uint256[]` is + an array type that composes an elementary type. Complex types in this schema + are polymorphic in how they represent this composition; see + [below](#complextypes-contains-field) for information about complex types' + `"contains"` field. + +### Type wrappers and type references + +This schema defines the concept of a type wrapper and the related concept of a +type reference. + +Type wrappers serve to encapsulate a type representation alongside other fields +in the same object, and to facilitate discriminating which polymorphic form is +used for a particular complex type. + +Type wrappers are any object of the form +`{ "type": , ...otherProperties }`, where `` is either a complete +type representation or a reference to another type by ID. + +
+Example type wrapper with complete type representation + +```javascript +// from a struct type (which defines member types) +{ + "member": "beneficiary", + "type": { + "kind": "address" + } +} +``` +
+ +
+Example type wrapper with reference by ID + +```javascript +{ + "type": { + "id": "" + } +} +``` + +
+ + +Note that **ethdebug/format/type/base** places no restriction on IDs other than +that they must be either a number or a string. Other components of this format +at-large may impose restrictions, however. + +#### Type reference schema + +A type reference is an object containing the single `"id"` field. This field +must be a string or a number. + + + +#### Type wrapper schema + + + typeof item === "object" && + !("$schema" in item) && + item["$ref"] === "#/$defs/Type"} + transform={ + ({ $ref, ...rest }, root) => ({ + ...rest, + type: "object", + title: root.$defs.Type.title + " [RECURSIVE]", + description: "The root Type schema" + }) + } + /> + +### ComplexType's `"contains"` field + +Complex types inherently compose at least one other type and may do so in one +of three forms: +- Complex types may compose exactly one other type +- Complex types may compose an ordered list of other types +- Complex types may compose an object mapping of specific other types by key +As described [above](#type-wrappers-and-type-references), complex types compose +other types. This composition occurs inside the `"contains"` field for all +complex types. + +#### Example complex types to show different forms + + + + This is an example array type, which composes exactly one other type. + + ```json + { + "kind": "array", + "contains": { + "type": { + "kind": "uint", + "bits": 256 + } + } + } + ``` + + + This is an example array type, which composes an ordered list of member + types. + + ```json + { + "kind": "struct", + "contains": [{ + "member": "balance", + "type": { + "kind": "uint", + "bits": 256 + } + }, { + "member": "scoreSheet", + "type": { + "id": "" + } + }] + } + ``` + + In this example, please note how this struct type represents member names + with a `"member"` field alongside the `"type"` field, and note how the + value of `"type"` can be either a complete representation or a reference + object in the form of `{ id }`. + + + This is an example mapping type, which composes an object mapping of types + by key. + ```json + { + "kind": "mapping", + "contains": { + "key": { + "type": { + "kind": "address" + } + }, + "value": { + "type": { + "kind": "uint", + "bits": 256 + } + } + } + } + ``` + + + +## Full base schema + + + typeof item === "object" && + !("$schema" in item) && + item["$ref"] === "schema:ethdebug/format/type/base"} + transform={ + ({ $ref, ...rest }, root) => ({ + ...rest, + type: "object", + title: root.title + " [RECURSIVE]", + description: "The root Type schema" + }) + } + /> +## Example schema extensions for particular types + +These examples show valid schemas that extend **ethdebug/format/types/base** +for particular kinds of types. + +_**Note**: These are just examples and may not +correspond to the canonical **ethdebug/format/type** schema._ + + + + + + + + + + + + + diff --git a/web/spec/type/overview.mdx b/web/spec/type/overview.mdx new file mode 100644 index 00000000..cb7bfbf2 --- /dev/null +++ b/web/spec/type/overview.mdx @@ -0,0 +1,98 @@ +--- +sidebar_position: 1 +--- + +# Overview + +:::tip +**ethdebug/format/type** defines how to write data types as JSON. + +Debuggers critically rely on having representations of the data types +used by a piece of code. This information is used to highlight code display, +offer links to where user-defined types are defined, and to render runtime +values correctly. + +For a quick introduction to type representations, please see these example +JSON values: + +
+A valid type representation + +```json +{ + "kind": "uint", + "bits": 256 +} +``` + +
+ +
+An invalid type representation + +```json +"uh, some kind of number" +``` + +
+ + +::: + + +This format defines schemas for representing the data types allowable in a +supporting high-level language. + +JSON values that adhere to this schema may (for example) represent a particular +`uint` type (like `uint256`), a `struct` type with a particular set of member +fields, a particular `mapping` type from a certain key type to a certain value +type, and so on. + +This schema is broadly divided into two sections: +1. A canonical Type schema + ([**ethdebug/format/type**](/spec/type)), which includes + subschemas for included known types. + + When adhering to this format, this schema is considered **sufficient** for + representing any supported type. + +2. A base Type schema + ([**ethdebug/format/type/base**](/spec/type)), which specifies a + minimal definition of any type, known or unknown. + + When adhering to this format, this schema is considered **necessary** for + representing any supported type. + +In other words: + +- Compilers adhering to this format **should** use the canonical + **ethdebug/format/type** schema when representing known types + (e.g., uints, arrays, structs, etc.). + +- Compilers **must** still adhere to the **ethdebug/format/type/base** schema + when representing types not known to this format. + +:::note +Any representation adhering to the former also adheres to the latter, +since **ethdebug/format/type** extends **ethdebug/format/type/base**. +::: + +:::info +To highlight one purpose behind this separation, consider that this format +seeks to be complete enough to be useful _and_ flexible enough to afford +extension. + +While **ethdebug/format/type** aims to cover all of the available kinds of +types available in EVM languages today, languages in the future may offer +additional kinds of types. **ethdebug/format/type/base** serves to address +this concern. +::: + + diff --git a/web/spec/type/type.mdx b/web/spec/type/type.mdx new file mode 100644 index 00000000..99c17d99 --- /dev/null +++ b/web/spec/type/type.mdx @@ -0,0 +1,21 @@ +--- +sidebar_position: 2 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; +import { Collapsible, CreateTypes } from "@theme/JSONSchemaViewer/components"; +import { schemas } from "@site/src/loadSchema"; + +# ethdebug/format/type [placeholder] + +:::note + +This schema remains unspecified. Please see the Type schemas +[Overview](/spec/schema/type/overview) for more information on how these +schemas will be organized, and/or please review the +[**ethdebug/format/type/base** schema](/spec/schema/type/base) that is intended +to serve as base subschema for **ethdebug/format/type**. + +We appreciate your interest in these developing efforts. + +::: diff --git a/web/src/components/SchemaViewer.tsx b/web/src/components/SchemaViewer.tsx index c06a1129..08cbc7ef 100644 --- a/web/src/components/SchemaViewer.tsx +++ b/web/src/components/SchemaViewer.tsx @@ -1,23 +1,96 @@ import JSONSchemaViewer from "@theme/JSONSchemaViewer"; +import CodeBlock from "@theme/CodeBlock"; import { loadSchema } from "@site/src/loadSchema"; +import ReactMarkdown from "react-markdown"; export interface SchemaViewerProps { - schema: string; + schema: string | object; pointer?: string; + detect?: (item: object) => boolean; + transform?: (item: object, root: object) => object; +} + + +export const transformObject = ( + obj: object, + predicate: (item: object) => boolean, + transform: (item: object, root: object) => object +): object => { + const process = (currentObj: object): object => { + if (predicate(currentObj)) { + return transform(currentObj, obj); + } + + if (typeof currentObj !== 'object' || currentObj === null) { + return currentObj; + } + + // Using Array.isArray to differentiate between array and object + if (Array.isArray(currentObj)) { + return currentObj.map(item => process(item)); + } else { + return Object.keys(currentObj).reduce((acc, key) => { + acc[key] = process(currentObj[key]); + return acc; + }, {}); + } + }; + + return process(obj); } export default function SchemaViewer({ - schema, - pointer = "" + schema: schemaName, + pointer = "", + detect = () => false, + transform = (x) => x }: SchemaViewerProps): JSX.Element { + const rawSchema = typeof schemaName === "string" + ? loadSchema(schemaName) + : schemaName; + const schema = transformObject( + rawSchema, + detect, + transform + ); + return ( { + const schema = loadSchema(uri.toString()); + return schema; + + } + } + } }} viewerOptions={{ - showExamples: true + showExamples: true, + ValueComponent: ({ value }) => { + // deal with simple types first + if ([ + "string", + "number", + "bigint", + "boolean" + ].includes(typeof value)) { + return { + (value as string | number | bigint | boolean).toString() + }; + } + + // for complex types use a whole CodeBlock + return {`${ + JSON.stringify(value, undefined, 2) + }`}; + }, + DescriptionComponent: ({description}) => + }} /> ); } diff --git a/web/src/loadSchema.ts b/web/src/loadSchema.ts index 8ba99f1e..7dcff92f 100644 --- a/web/src/loadSchema.ts +++ b/web/src/loadSchema.ts @@ -1,4 +1,8 @@ +import typeBaseSchemaYaml from "../../schemas/type/base.schema.yaml"; + + export const schemas = [ + typeBaseSchemaYaml, ].map(schema => ({ [schema.$id]: schema })).reduce((a, b) => ({ ...a, ...b }), {});