diff --git a/.prettierignore b/.prettierignore index b0eaf397c7..e8f1a65137 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ package-lock.json # Auto-generated files packages/format/src/schemas/yamls.ts +packages/format/src/version.ts packages/bugc/src/examples/generated.ts # Solidity fixtures are compiler inputs; this repo has no Solidity Prettier parser. diff --git a/CHANGELOG.md b/CHANGELOG.md index 651577513d..5d805c5c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,30 @@ The sections do not signal obligations; the prefixes do. ## Unreleased +### Added + +- A stamp in an `ethdebug` field on **ethdebug/format/info**, + **ethdebug/format/info/resources** and **ethdebug/format/program** names + the schema the object conforms to and the specification version that + defines it. The new **ethdebug/format/data/stamp** schema defines the + stamp. Info documents and resources objects must carry the stamp + ([#305]). + - Schemas: **ethdebug/format/data/stamp**, + **ethdebug/format/info**, **ethdebug/format/info/resources**, + **ethdebug/format/program** + - Producers: required: **ethdebug/format/info** and + **ethdebug/format/info/resources** list `ethdebug` in `required`, so + an info document or resources object must carry + `ethdebug: { schema, version }`. A program emitted outside an info + document **should** carry the stamp; a program inside an info + document **should not**. All objects of one compilation must name the + same `version`. + - Consumers: required: a consumer that validates data against the + schemas must use this version's schemas. **ethdebug/format/program** + and **ethdebug/format/info** are closed objects + (`unevaluatedProperties: false`), so the previous version's schemas + reject the `ethdebug` key. + ### Changed - The `offset` of a segment counts bytes from the most significant byte of the @@ -632,4 +656,5 @@ First published version of the specification. [#285]: https://github.com/ethdebug/format/pull/285 [#286]: https://github.com/ethdebug/format/pull/286 [#303]: https://github.com/ethdebug/format/pull/303 +[#305]: https://github.com/ethdebug/format/pull/305 [#309]: https://github.com/ethdebug/format/pull/309 diff --git a/RELEASING.md b/RELEASING.md index 5c72d5f445..f4dcd33d9c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -130,6 +130,21 @@ guards that run in CI live in `bin/check-tarballs.ts` and yarn tsx bin/version.ts [keyword] [--all] ``` + When `@ethdebug/format` moves, the `Publish` commit also rewrites + the specification version in the schema examples, so `schemas/` may + appear in that commit beside the manifests; the dry run prints the + count of version literals it rewrites. + + A `schemas/: no example names the specification version` problem + means every `ethdebug` block was removed from the examples; a + `names X, expected Y` problem means one drifted from the version + `@ethdebug/format` carries. Edit the example and re-run. + + After the bump, run `yarn build` before running + `yarn test packages/format` again: the generated + `src/version.ts` still names the old version until the build + regenerates it. CI does this step in the publish workflow. + The script never pushes. If it fails after it started writing, it prints the undo commands for the stage it reached. The dry run of step 3 reports the same guards and findings as this run, but it diff --git a/bin/release/schema-versions.test.ts b/bin/release/schema-versions.test.ts new file mode 100644 index 0000000000..2a323c32a9 --- /dev/null +++ b/bin/release/schema-versions.test.ts @@ -0,0 +1,97 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +import { + checkVersions, + readSchemas, + setVersions, + versionSites, +} from "./schema-versions.js"; + +const root = fileURLToPath(new URL("../..", import.meta.url)); +const read = (path: string) => readFileSync(join(root, path), "utf8"); + +const info = read("schemas/info.schema.yaml"); +const program = read("schemas/program.schema.yaml"); +const stamp = read("schemas/data/stamp.schema.yaml"); +const hex = read("schemas/data/hex.schema.yaml"); + +const lineAt = (text: string, index: number) => + text.slice(0, index).split("\n").length; + +describe("versionSites", () => { + it("finds the nested ethdebug block and ignores compiler versions", () => { + expect(info).toMatch(/^\s+version: 0\.2\.3/m); + const sites = versionSites(info); + expect(sites).toHaveLength(1); + expect(sites[0].path).toMatch(/^examples\/\d+\/ethdebug$/); + }); + + it("finds an example that is itself a stamp", () => { + expect(versionSites(stamp).map((s) => s.path)).toEqual(["examples/0"]); + }); + + it("ignores the ethdebug property that declares the field", () => { + expect(program).toMatch(/^properties:\n(.*\n)*? {2}ethdebug:/m); + expect(versionSites(program).map((s) => s.path)).toEqual([ + "examples/0/ethdebug", + ]); + }); + + it("reports the line and the range of the quoted scalar", () => { + const [site] = versionSites(program); + const [start, end] = site.range; + expect(program.slice(start, end)).toBe(`"${site.version}"`); + expect(site.line).toBe(lineAt(program, start)); + }); +}); + +describe("setVersions", () => { + it("sets the site and leaves everything else byte for byte", () => { + const [start, end] = versionSites(program)[0].range; + const after = setVersions(program, "0.1.0-draft.1"); + expect(after.slice(0, start)).toBe(program.slice(0, start)); + expect(after.slice(start, after.length - (program.length - end))).toBe( + '"0.1.0-draft.1"', + ); + expect(after.endsWith(program.slice(end))).toBe(true); + }); + + it("returns the text unchanged when there is no site", () => { + expect(versionSites(hex)).toEqual([]); + expect(setVersions(hex, "0.2.0")).toBe(hex); + }); +}); + +describe("checkVersions", () => { + const path = "schemas/program.schema.yaml"; + const sites = versionSites(program); + const file = { path, text: program, sites }; + + it("names the file, the line and both versions when one differs", () => { + const [problem] = checkVersions(file, "0.9.9"); + expect(problem).toContain(`${path}:${sites[0].line}`); + expect(problem).toContain(sites[0].version); + expect(problem).toContain("0.9.9"); + }); +}); + +describe("the schemas in this repository", () => { + const packageVersion = JSON.parse(read("packages/format/package.json")) + .version as string; + + it("carry at least one version site", () => { + const sites = readSchemas(root).flatMap((file) => file.sites); + expect(sites.length).toBeGreaterThan(0); + }); + + it("name the version @ethdebug/format carries", () => { + const problems = readSchemas(root).flatMap((file) => + checkVersions(file, packageVersion), + ); + expect(problems).toEqual([]); + }); +}); diff --git a/bin/release/schema-versions.ts b/bin/release/schema-versions.ts new file mode 100644 index 0000000000..249e752c0c --- /dev/null +++ b/bin/release/schema-versions.ts @@ -0,0 +1,116 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +import { isMap, isScalar, isSeq, parseDocument } from "yaml"; + +// a `version` scalar that a release rewrites: the version field of a +// stamp sitting inside a schema's top-level `examples` +export interface Site { + // a path like "examples/0/ethdebug", for messages + path: string; + line: number; + version: string; + // byte range of the scalar token, quotes included + range: [number, number]; +} + +export interface SchemaFile { + // relative to the repository root + path: string; + text: string; + sites: Site[]; +} + +const schemaNamePrefix = "ethdebug/format/"; + +// a stamp is any mapping with a `schema` value that starts with +// `ethdebug/format/` and a `version` beside it. +function collect(node: unknown, path: string, text: string, out: Site[]) { + if (isMap(node)) { + const schema = node.get("schema"); + const version = node.get("version", true); + if ( + typeof schema === "string" && + schema.startsWith(schemaNamePrefix) && + isScalar(version) && + typeof version.value === "string" && + version.range + ) { + out.push({ + path, + line: text.slice(0, version.range[0]).split("\n").length, + version: version.value, + range: [version.range[0], version.range[1]], + }); + } + for (const item of node.items) { + const key = String( + (isScalar(item.key) ? item.key.value : undefined) ?? "?", + ); + collect(item.value, `${path}/${key}`, text, out); + } + } else if (isSeq(node)) { + node.items.forEach((item, index) => + collect(item, `${path}/${index}`, text, out), + ); + } +} + +export function versionSites(text: string): Site[] { + const examples = parseDocument(text).get("examples", true); + const out: Site[] = []; + if (examples) { + collect(examples, "examples", text, out); + } + return out.sort((a, b) => a.range[0] - b.range[0]); +} + +// splices each site into the original bytes, so comments, spacing and +// the long block descriptions survive exactly. +export function setVersions(text: string, to: string): string { + let result = ""; + let cursor = 0; + for (const site of versionSites(text)) { + const [start, end] = site.range; + const original = text.slice(start, end); + const quote = original.startsWith('"') + ? '"' + : original.startsWith("'") + ? "'" + : ""; + result += text.slice(cursor, start) + `${quote}${to}${quote}`; + cursor = end; + } + return result + text.slice(cursor); +} + +// every site must already name the version the package carries; one +// that does not means a schema example drifted from the release +export function checkVersions(file: SchemaFile, expected: string): string[] { + return file.sites + .filter((site) => site.version !== expected) + .map( + (site) => + `${file.path}:${site.line}: ${site.path} names ` + + `${site.version}, expected ${expected}`, + ); +} + +function schemaPaths(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + return schemaPaths(path); + } + return entry.name.endsWith(".schema.yaml") ? [path] : []; + }); +} + +export function readSchemas(root: string): SchemaFile[] { + return schemaPaths(join(root, "schemas")) + .sort() + .map((path) => { + const text = readFileSync(path, "utf8"); + return { path: relative(root, path), text, sites: versionSites(text) }; + }); +} diff --git a/bin/version.test.ts b/bin/version.test.ts index e915f1d386..522405822d 100644 --- a/bin/version.test.ts +++ b/bin/version.test.ts @@ -608,7 +608,7 @@ describe("undoAdvice", () => { it("restores the manifests when nothing was committed or tagged", () => { expect(undoAdvice([], false)).toBe( - "undo: git checkout HEAD -- packages/*/package.json", + "undo: git checkout HEAD -- packages/*/package.json schemas/", ); }); }); diff --git a/bin/version.ts b/bin/version.ts index 3a3e0607b3..7abef7925d 100644 --- a/bin/version.ts +++ b/bin/version.ts @@ -5,6 +5,12 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import semver from "semver"; +import { + checkVersions, + readSchemas, + setVersions, +} from "./release/schema-versions.js"; + // the schemas ship inside this package, so its version is the version // of the specification export const specPackage = "@ethdebug/format"; @@ -636,7 +642,7 @@ export function undoAdvice(created: string[], committed: boolean): string { if (tags.length > 0) { return `undo: ${tags}`; } - return "undo: git checkout HEAD -- packages/*/package.json"; + return "undo: git checkout HEAD -- packages/*/package.json schemas/"; } function report(plan: Move[]): void { @@ -681,6 +687,32 @@ export function main(argv: string[]): number { all, }); const problems = planProblems(plan, manifests, keyword); + // the schemas ship inside @ethdebug/format, so their examples name + // the version it moves to; when it stays put they are left alone + const specMove = plan.find((move) => move.name === specPackage); + let schemas: { path: string; text: string }[] = []; + let siteCount = 0; + if (specMove !== undefined) { + const files = readSchemas(root); + siteCount = files.flatMap((file) => file.sites).length; + if (siteCount === 0) { + problems.push( + "schemas/: no example names the specification version; " + + "the release would rewrite nothing", + ); + } + problems.push( + ...files.flatMap((file) => checkVersions(file, specMove.from)), + ); + schemas = files + .map((file) => ({ + path: file.path, + text: setVersions(file.text, specMove.to), + before: file.text, + })) + .filter((file) => file.text !== file.before) + .map(({ path, text }) => ({ path, text })); + } if (problems.length > 0) { for (const problem of problems) { console.error(problem); @@ -700,6 +732,10 @@ export function main(argv: string[]): number { } console.log(`${keyword}: ${plan.length} workspace(s) move`); report(plan); + if (specMove !== undefined) { + const literals = `${siteCount} version literals`; + console.log(` schemas: ${literals} -> ${specMove.to}`); + } const changelogs = changelogProblems( requiredChangelogs(plan, manifests, root).map(({ path, version }) => ({ @@ -729,9 +765,15 @@ export function main(argv: string[]): number { // every release const headBefore = git(root, ["rev-parse", "HEAD"]); let written: string[] = []; + let schemaCount = 0; const created: string[] = []; try { written = writeManifests(root, manifests, plan); + for (const { path, text } of schemas) { + writeFileSync(join(root, path), text); + written.push(path); + } + schemaCount = schemas.length; commitAndTag(root, written, plan, created); } catch (error) { const message = error instanceof Error ? error.message : String(error); @@ -742,7 +784,12 @@ export function main(argv: string[]): number { } console.log(`tagged: ${created.join(", ")}`); if (written.length > 0) { - console.log(`committed Publish with ${written.length} manifest(s)`); + const schemaNote = + schemaCount > 0 ? ` and ${schemaCount} schema file(s)` : ""; + console.log( + `committed Publish with ${written.length - schemaCount} ` + + `manifest(s)${schemaNote}`, + ); console.log("next: git push --atomic origin main --follow-tags"); return 0; } diff --git a/eslint.config.js b/eslint.config.js index f375f13f1b..8e4aa8874b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -72,6 +72,7 @@ export default tseslint.config( "**/*.config.js", "**/*.config.ts", "packages/format/src/schemas/yamls.ts", + "packages/format/src/version.ts", "packages/bugc/src/examples/generated.ts", "packages/web/.docusaurus/", "packages/web/build/", diff --git a/package.json b/package.json index 78237272cd..2bda9ce6cf 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "semver": "^7.7.3", "tsx": "^4.21.0", "typescript-eslint": "^8.53.0", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "yaml": "^2.8.2" }, "lint-staged": { "*.{ts,tsx}": [ diff --git a/packages/bugc/CHANGELOG.md b/packages/bugc/CHANGELOG.md index 01d47d0d6e..c86e2cae1d 100644 --- a/packages/bugc/CHANGELOG.md +++ b/packages/bugc/CHANGELOG.md @@ -7,6 +7,12 @@ support. Changes to the specification itself are tracked in the root ## Unreleased +### Changed + +- Every program `bugc` emits carries a stamp in its `ethdebug` field, + naming **ethdebug/format/program** and the specification version + ([#305]). + ## 0.1.0-preview.0 — 2026-09-21 The version scheme changed: prerelease versions are now `preview.`, and @@ -35,3 +41,4 @@ First publication. [#286]: https://github.com/ethdebug/format/pull/286 [#298]: https://github.com/ethdebug/format/pull/298 [#300]: https://github.com/ethdebug/format/pull/300 +[#305]: https://github.com/ethdebug/format/pull/305 diff --git a/packages/bugc/src/evmgen/program-builder.test.ts b/packages/bugc/src/evmgen/program-builder.test.ts new file mode 100644 index 0000000000..f0d9aa5e38 --- /dev/null +++ b/packages/bugc/src/evmgen/program-builder.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; + +import * as Format from "@ethdebug/format"; +import * as Ir from "#ir"; + +import { buildProgram } from "./program-builder.js"; + +describe("buildProgram", () => { + it("stamps the program with its schema and the specification version", () => { + const module: Ir.Module = { + name: "Test", + sourceId: "test", + functions: new Map(), + main: { + name: "main", + parameters: [], + entry: "entry", + blocks: new Map([ + [ + "entry", + { + id: "entry", + phis: [], + instructions: [], + terminator: { kind: "return", operationDebug: {} }, + predecessors: new Set(), + debug: {}, + } as Ir.Block, + ], + ]), + }, + }; + + const program = buildProgram([], "call", module); + + expect(program.ethdebug).toEqual({ + schema: "ethdebug/format/program", + version: Format.version, + }); + }); +}); diff --git a/packages/bugc/src/evmgen/program-builder.ts b/packages/bugc/src/evmgen/program-builder.ts index cf33e20851..165ddfa8c2 100644 --- a/packages/bugc/src/evmgen/program-builder.ts +++ b/packages/bugc/src/evmgen/program-builder.ts @@ -2,7 +2,7 @@ * Build Format.Program objects from EVM generation output */ -import type * as Format from "@ethdebug/format"; +import * as Format from "@ethdebug/format"; import type * as Evm from "#evm"; import type * as Ir from "#ir"; @@ -81,11 +81,11 @@ export function buildProgram( }, }; - const program: Format.Program = { + const program: Format.Program = Format.Data.stamp("ethdebug/format/program", { contract, environment, instructions: formatInstructions, - }; + }); // Add program-level context if available if (ir.debugContext) { diff --git a/packages/conformance/src/adapters/bugc.ts b/packages/conformance/src/adapters/bugc.ts index d28b511f4e..84f444e899 100644 --- a/packages/conformance/src/adapters/bugc.ts +++ b/packages/conformance/src/adapters/bugc.ts @@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import type { Materials } from "@ethdebug/format"; +import { Data } from "@ethdebug/format"; import { VERSION, compile } from "@ethdebug/bugc"; import type { BugcCompileOptions, EthdebugArtifact } from "../types.js"; @@ -112,11 +113,11 @@ export async function compileBugc( sources, programs, compilation, - resources: { + resources: Data.stamp("ethdebug/format/info/resources", { compilation, types: {}, pointers: {}, - }, + }), raw: result.value, }; } diff --git a/packages/conformance/src/adapters/soldb.ts b/packages/conformance/src/adapters/soldb.ts index ff2f2401ee..7616dc9fd8 100644 --- a/packages/conformance/src/adapters/soldb.ts +++ b/packages/conformance/src/adapters/soldb.ts @@ -3,6 +3,8 @@ import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { Data } from "@ethdebug/format"; + import type { EthdebugArtifact, SoldbCommand, @@ -100,11 +102,11 @@ function ethdebugResources(artifact: EthdebugArtifact): unknown { } if (artifact.compilation) { - return { + return Data.stamp("ethdebug/format/info/resources", { compilation: artifact.compilation, types: {}, pointers: {}, - }; + }); } throw new Error( diff --git a/packages/conformance/src/runner.ts b/packages/conformance/src/runner.ts index 64d5bfcaf4..d3d34686f0 100644 --- a/packages/conformance/src/runner.ts +++ b/packages/conformance/src/runner.ts @@ -149,6 +149,22 @@ export async function validateStaticConformance( ); } + const resourcesVersion = artifact.resources?.ethdebug?.version; + if (resourcesVersion !== undefined) { + artifact.programs.forEach((program, index) => { + const programVersion = program.program.ethdebug?.version; + if (programVersion !== undefined && programVersion !== resourcesVersion) { + issues.push( + issue( + `programs[${index}].ethdebug.version`, + `${program.name} names ethdebug/format ${programVersion} but ` + + `resources names ${resourcesVersion}`, + ), + ); + } + }); + } + if (artifact.compilation && !Materials.isCompilation(artifact.compilation)) { issues.push( issue("compilation", "compilation is not valid materials/compilation"), diff --git a/packages/conformance/src/types.ts b/packages/conformance/src/types.ts index 14910bb65a..eb294069ea 100644 --- a/packages/conformance/src/types.ts +++ b/packages/conformance/src/types.ts @@ -1,4 +1,4 @@ -import type { Materials, Program } from "@ethdebug/format"; +import type { Data, Materials, Program } from "@ethdebug/format"; export type CompilerKind = "bugc" | "solc"; @@ -20,6 +20,7 @@ export interface EthdebugArtifact { programs: EthdebugProgramArtifact[]; compilation?: Materials.Compilation; resources?: { + ethdebug?: Data.Stamp; compilation: Materials.Compilation; types: Record; pointers: Record; diff --git a/packages/conformance/test/conformance.test.ts b/packages/conformance/test/conformance.test.ts index 1150cf394e..7cbbc7add0 100644 --- a/packages/conformance/test/conformance.test.ts +++ b/packages/conformance/test/conformance.test.ts @@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; +import { version } from "@ethdebug/format"; + import { deployBytecode, sendContractTransaction, @@ -100,6 +102,10 @@ function validProgram() { function validResources() { return { + ethdebug: { + schema: "ethdebug/format/info/resources", + version: "0.1.0-draft.0", + }, compilation: validCompilation(), types: { CounterSlot: { @@ -119,6 +125,50 @@ function validResources() { }; } +function programWithVersion(version: string) { + return { + ...validProgram(), + ethdebug: { schema: "ethdebug/format/program", version }, + }; +} + +function resourcesWithVersion(version: string) { + return { + ...validResources(), + ethdebug: { schema: "ethdebug/format/info/resources", version }, + }; +} + +// Expected failure (#311): solc does not emit the stamp on its resources +// object yet. Until it does, the solc tests add the stamp to a copy of the +// artifact before they validate it, so the rest of resources is still +// checked. The check below fails once solc emits the stamp; then remove +// this function and validate the artifact as it is. +function withSolcResourcesStamp(artifact: EthdebugArtifact): EthdebugArtifact { + expect( + artifact.resources?.ethdebug, + "solc now emits the stamp on its resources object; " + + "remove withSolcResourcesStamp", + ).toBeUndefined(); + if (!artifact.resources) { + return artifact; + } + + const programVersion = artifact.programs.find( + ({ program }) => program.ethdebug, + )?.program.ethdebug?.version; + return { + ...artifact, + resources: { + ...artifact.resources, + ethdebug: { + schema: "ethdebug/format/info/resources", + version: programVersion ?? version, + }, + }, + }; +} + function validArtifact( overrides: Partial = {}, ): EthdebugArtifact { @@ -164,7 +214,9 @@ describe("@ethdebug/conformance", () => { sourcePath: path.join(root, "test/fixtures/solc/Counter.sol"), }); - const result = await validateStaticConformance(artifact); + const result = await validateStaticConformance( + withSolcResourcesStamp(artifact), + ); expect(result.issues).toEqual([]); expect(result.ok).toBe(true); expect( @@ -215,7 +267,9 @@ describe("@ethdebug/conformance", () => { path.join(sourceDir, "Math.sol"), ], }); - const result = await validateStaticConformance(artifact); + const result = await validateStaticConformance( + withSolcResourcesStamp(artifact), + ); expect(result.issues).toEqual([]); expect(result.ok).toBe(true); @@ -359,6 +413,21 @@ describe("@ethdebug/conformance", () => { expect(result.ok).toBe(true); }); + it("rejects resources without the stamp", async () => { + const { ethdebug: _, ...resources } = validResources(); + const artifact = validArtifact({ + compilation: undefined, + resources: resources as any, + }); + + const result = await validateStaticConformance(artifact); + + expect(result.ok).toBe(false); + expect(result.issues.some((issue) => issue.path === "resources")).toBe( + true, + ); + }); + it("rejects malformed resources lookup tables through JSON-Schema validation", async () => { const artifact = validArtifact({ compilation: undefined, @@ -380,6 +449,68 @@ describe("@ethdebug/conformance", () => { ); }); + it("rejects mismatched versions", async () => { + const artifact = validArtifact({ + compilation: undefined, + programs: [ + { + name: "Counter:runtime", + program: programWithVersion("0.1.0-draft.0") as any, + }, + ], + resources: resourcesWithVersion("0.1.0-draft.1") as any, + }); + + const result = await validateStaticConformance(artifact); + + expect(result.ok).toBe(false); + expect( + result.issues.some( + (issue) => issue.path === "programs[0].ethdebug.version", + ), + ).toBe(true); + }); + + it("accepts equal versions", async () => { + const artifact = validArtifact({ + compilation: undefined, + programs: [ + { + name: "Counter:runtime", + program: programWithVersion("0.1.0-draft.0") as any, + }, + ], + resources: resourcesWithVersion("0.1.0-draft.0") as any, + }); + + const result = await validateStaticConformance(artifact); + + expect(result.issues).toEqual([]); + expect(result.ok).toBe(true); + }); + + it("accepts programs without the stamp", async () => { + const result = await validateStaticConformance(validArtifact()); + + expect(result.issues).toEqual([]); + expect(result.ok).toBe(true); + }); + + it("stamps the resources it synthesizes for SolDB", async () => { + const debugDir = await writeSoldbDebugDir(validArtifact(), { + contractName: "Counter", + }); + + const resources = JSON.parse( + await readFile(path.join(debugDir.debugDir, "ethdebug.json"), "utf8"), + ); + + expect(resources.ethdebug).toEqual({ + schema: "ethdebug/format/info/resources", + version, + }); + }); + it("materializes non-empty resources into SolDB debug directories", async () => { const debugDir = await writeSoldbDebugDir( validArtifact({ diff --git a/packages/format/.gitignore b/packages/format/.gitignore index 596a165405..4a20c9168f 100644 --- a/packages/format/.gitignore +++ b/packages/format/.gitignore @@ -1,2 +1,3 @@ dist src/schemas/yamls.ts +src/version.ts diff --git a/packages/format/CHANGELOG.md b/packages/format/CHANGELOG.md index 04f0823b3f..c2443529bc 100644 --- a/packages/format/CHANGELOG.md +++ b/packages/format/CHANGELOG.md @@ -6,6 +6,16 @@ tracked in the root [`CHANGELOG.md`](../../CHANGELOG.md). ## Unreleased +### Added + +- `version`, the package's own version string, generated at build time from + `package.json` ([#305]). +- `Data.Stamp` and `Data.isStamp`, the type and guard for the new + **ethdebug/format/data/stamp** schema; `Data.stamp(schema, object)` + returns the object with a stamp for this package's version in its + `ethdebug` field ([#305]). +- `Program.ethdebug`, an optional `Data.Stamp` ([#305]). + ## 0.1.0-draft.0 — 2026-09-21 The version scheme changed: prerelease versions are now `draft.`, matching @@ -116,6 +126,7 @@ First publication. [#293]: https://github.com/ethdebug/format/pull/293 [#300]: https://github.com/ethdebug/format/pull/300 [#303]: https://github.com/ethdebug/format/pull/303 +[#305]: https://github.com/ethdebug/format/pull/305 [`0ef2f37`]: https://github.com/ethdebug/format/commit/0ef2f37 [`10ab103`]: https://github.com/ethdebug/format/commit/10ab103 [`21e532e`]: https://github.com/ethdebug/format/commit/21e532e diff --git a/packages/format/bin/generate-schema-yamls.js b/packages/format/bin/generate-schema-yamls.js index 181b41a859..2320d832fb 100644 --- a/packages/format/bin/generate-schema-yamls.js +++ b/packages/format/bin/generate-schema-yamls.js @@ -61,3 +61,13 @@ const tempPath = outputPath + ".tmp"; // Write to temp file, then rename atomically to avoid race conditions fs.writeFileSync(tempPath, output); fs.renameSync(tempPath, outputPath); + +const packageJson = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8"), +); +const versionOutput = `// THIS FILE GETS AUTO-GENERATED AS PART OF THIS PACKAGE'S BUILD PROCESS +// Please do not modify it directly or allow it to get checked into source control. + +export const version: string = ${JSON.stringify(packageJson.version)}; +`; +fs.writeFileSync(path.resolve(__dirname, "../src/version.ts"), versionOutput); diff --git a/packages/format/package.json b/packages/format/package.json index 9188db2b8c..9df289b517 100644 --- a/packages/format/package.json +++ b/packages/format/package.json @@ -51,13 +51,17 @@ "types": "./src/types/type/index.ts", "default": "./dist/src/types/type/index.js" }, + "#version": { + "types": "./src/version.ts", + "default": "./dist/src/version.js" + }, "#test/*": "./test/*.ts" }, "scripts": { "prepare:yamls": "node ./bin/generate-schema-yamls.js", "build": "yarn prepare:yamls && rm -rf dist && tsc --build tsconfig.build.json", "prepare": "yarn build", - "clean": "rm -rf dist && rm src/schemas/yamls.ts", + "clean": "rm -rf dist && rm -f src/schemas/yamls.ts src/version.ts", "typecheck": "tsc -p tsconfig.typecheck.json", "test": "vitest", "watch:typescript": "tsc --watch", diff --git a/packages/format/src/index.ts b/packages/format/src/index.ts index e39b6f9ba1..f16907fda8 100644 --- a/packages/format/src/index.ts +++ b/packages/format/src/index.ts @@ -2,3 +2,5 @@ export * from "#describe"; export { schemas, schemaIds, type Schema } from "#schemas"; export * from "#types"; + +export { version } from "#version"; diff --git a/packages/format/src/schemas/stamp.test.ts b/packages/format/src/schemas/stamp.test.ts new file mode 100644 index 0000000000..cce1399b7c --- /dev/null +++ b/packages/format/src/schemas/stamp.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; +import "#test/hyperjump"; + +const program = { + contract: { + name: "A", + definition: { source: { id: 0 }, range: { offset: 0, length: 1 } }, + }, + environment: "call", + instructions: [{ offset: 0 }], +}; +const id = (schema: string, version = "0.1.0-draft.0") => ({ + schema, + version, +}); + +// from the example in schemas/info.schema.yaml +const compilation = { + id: "__301f3b6d85831638", + compiler: { + name: "egc", + version: "0.2.3+commit.8b37fa7a", + }, + settings: { + turbo: true, + }, + sources: [ + { + id: 1, + path: "Escrow.eg", + language: "examplelang", + contents: "func main(): return", + }, + ], +}; + +describe("stamp", () => { + it("accepts a program that names its own schema", async () => { + await expect({ + ethdebug: id("ethdebug/format/program"), + ...program, + }).toValidate({ schema: { id: "schema:ethdebug/format/program" } }); + }); + + it("accepts a program without the field", async () => { + await expect(program).toValidate({ + schema: { id: "schema:ethdebug/format/program" }, + }); + }); + + it("rejects a program that claims another schema", async () => { + await expect({ + ethdebug: id("ethdebug/format/info"), + ...program, + }).not.toValidate({ schema: { id: "schema:ethdebug/format/program" } }); + }); + + it("rejects versions the pattern forbids", async () => { + for (const version of [ + "0.1", + "v0.1.0", + "01.1.0", + "0.1.0-draft.01", + "0.1.0+build", + "0.1.0-", + ]) { + await expect({ + ethdebug: id("ethdebug/format/program", version), + ...program, + }).not.toValidate({ schema: { id: "schema:ethdebug/format/program" } }); + } + }); + + it("accepts numeric and named prereleases and stable versions", async () => { + for (const version of ["0.1.0-2", "0.1.0-draft.3", "0.1.0", "1.0.0-rc.1"]) { + await expect({ + ethdebug: id("ethdebug/format/program", version), + ...program, + }).toValidate({ schema: { id: "schema:ethdebug/format/program" } }); + } + }); + + it("rejects an extra key inside the field", async () => { + await expect({ + ethdebug: { ...id("ethdebug/format/program"), extra: 1 }, + ...program, + }).not.toValidate({ schema: { id: "schema:ethdebug/format/program" } }); + }); + + const resources = { types: {}, pointers: {} }; + const info = { compilation, programs: [], ...resources }; + + it("accepts resources that name resources", async () => { + await expect({ + ethdebug: id("ethdebug/format/info/resources"), + ...resources, + }).toValidate({ schema: { id: "schema:ethdebug/format/info/resources" } }); + }); + + it("rejects resources without the field", async () => { + await expect(resources).not.toValidate({ + schema: { id: "schema:ethdebug/format/info/resources" }, + }); + }); + + it("rejects resources that name info", async () => { + await expect({ + ethdebug: id("ethdebug/format/info"), + ...resources, + }).not.toValidate({ + schema: { id: "schema:ethdebug/format/info/resources" }, + }); + }); + + it("accepts an info document that names info", async () => { + await expect({ + ethdebug: id("ethdebug/format/info"), + ...info, + }).toValidate({ schema: { id: "schema:ethdebug/format/info" } }); + }); + + it("rejects an info document without the field", async () => { + await expect(info).not.toValidate({ + schema: { id: "schema:ethdebug/format/info" }, + }); + }); + + it("rejects an info document that names resources", async () => { + await expect({ + ethdebug: id("ethdebug/format/info/resources"), + ...info, + }).not.toValidate({ schema: { id: "schema:ethdebug/format/info" } }); + }); +}); diff --git a/packages/format/src/types/data/index.test.ts b/packages/format/src/types/data/index.test.ts index 1060badc6b..e59adfef50 100644 --- a/packages/format/src/types/data/index.test.ts +++ b/packages/format/src/types/data/index.test.ts @@ -1,4 +1,11 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { parse } from "yaml"; +import { describe, expect, expectTypeOf, it } from "vitest"; + import { testSchemaGuards } from "#test/guards"; +import { isProgram, type Program } from "#types/program"; +import { version } from "#version"; import { Data } from "./index.js"; @@ -15,4 +22,75 @@ testSchemaGuards("ethdebug/format/data", [ schema: "schema:ethdebug/format/data/hex", guard: Data.isHex, }, + { + schema: "schema:ethdebug/format/data/stamp", + guard: Data.isStamp, + }, ]); + +describe("stamp", () => { + it("puts the schema and this package's version first", () => { + const stamped = Data.stamp("ethdebug/format/info/resources", { + types: {}, + pointers: {}, + }); + expect(stamped).toEqual({ + ethdebug: { schema: "ethdebug/format/info/resources", version }, + types: {}, + pointers: {}, + }); + expect(Object.keys(stamped)).toEqual(["ethdebug", "types", "pointers"]); + }); + + it("carries the literal schema name in its type", () => { + const stamped = Data.stamp("ethdebug/format/program", { x: 1 }); + expectTypeOf( + stamped.ethdebug.schema, + ).toEqualTypeOf<"ethdebug/format/program">(); + expectTypeOf(stamped.x).toEqualTypeOf(); + }); + + it("builds a Program", () => { + const program: Program = Data.stamp("ethdebug/format/program", { + contract: { + name: "A", + definition: { source: { id: 0 }, range: { offset: 0, length: 1 } }, + }, + environment: "call", + instructions: [{ offset: 0 }], + }); + expect(isProgram(program)).toBe(true); + }); +}); + +describe("isStamp", () => { + it("accepts a schema name and a semver version", () => { + expect( + Data.isStamp({ + schema: "ethdebug/format/program", + version: "0.1.0-draft.0", + }), + ).toBe(true); + expect(Data.isStamp({ schema: "x", version: "0.1.0-2" })).toBe(true); + }); + + it("rejects what the schema pattern rejects", () => { + for (const v of ["v0.1.0", "0.1.0+build", " 0.1.0", "01.1.0", "0.1"]) { + expect(Data.isStamp({ schema: "x", version: v })).toBe(false); + } + expect(Data.isStamp({ schema: "x" })).toBe(false); + expect(Data.isStamp({ schema: "x", version: "0.1.0", extra: 1 })).toBe( + false, + ); + }); +}); + +describe("versionPattern", () => { + it("matches the pattern in schemas/data/stamp.schema.yaml", () => { + const schemaPath = fileURLToPath( + new URL("../../../../../schemas/data/stamp.schema.yaml", import.meta.url), + ); + const parsed = parse(readFileSync(schemaPath, "utf8")); + expect(Data.versionPattern.source).toBe(parsed.properties.version.pattern); + }); +}); diff --git a/packages/format/src/types/data/index.ts b/packages/format/src/types/data/index.ts index cc2662191b..56d7967e5e 100644 --- a/packages/format/src/types/data/index.ts +++ b/packages/format/src/types/data/index.ts @@ -1,3 +1,5 @@ +import { version } from "#version"; + export namespace Data { export type Value = Unsigned | Hex; @@ -15,4 +17,35 @@ export namespace Data { export const isHex = (value: unknown): value is Hex => typeof value === "string" && hexPattern.test(value); + + export interface Stamp { + schema: + | "ethdebug/format/info" + | "ethdebug/format/info/resources" + | "ethdebug/format/program"; + version: string; + } + + // the given object, stamped with the given root schema and this version + export const stamp = ( + schema: S, + unstamped: O, + ): { ethdebug: Stamp & { schema: S } } & O => ({ + ethdebug: { schema, version }, + ...unstamped, + }); + + // the pattern from schemas/data/stamp.schema.yaml + export const versionPattern = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$/; + + export const isStamp = (value: unknown): value is Stamp => + typeof value === "object" && + !!value && + "schema" in value && + typeof value.schema === "string" && + "version" in value && + typeof value.version === "string" && + versionPattern.test(value.version) && + Object.keys(value).length === 2; } diff --git a/packages/format/src/types/program/program.ts b/packages/format/src/types/program/program.ts index 62ff41ae46..0941f0615f 100644 --- a/packages/format/src/types/program/program.ts +++ b/packages/format/src/types/program/program.ts @@ -1,3 +1,4 @@ +import { Data } from "#types/data"; import { Materials } from "#types/materials"; import { Context as _Context, isContext as _isContext } from "./context.js"; @@ -8,6 +9,7 @@ import { } from "./instruction.js"; export interface Program { + ethdebug?: Data.Stamp; compilation?: Materials.Reference; contract: Program.Contract; environment: Program.Environment; @@ -27,7 +29,8 @@ export const isProgram = (value: unknown): value is Program => value.instructions.every(Program.isInstruction) && (!("compilation" in value) || Materials.isReference(value.compilation)) && - (!("context" in value) || Program.isContext(value.context)); + (!("context" in value) || Program.isContext(value.context)) && + (!("ethdebug" in value) || Data.isStamp(value.ethdebug)); export namespace Program { export import Context = _Context; diff --git a/packages/format/src/version.test.ts b/packages/format/src/version.test.ts new file mode 100644 index 0000000000..1a5f25789a --- /dev/null +++ b/packages/format/src/version.test.ts @@ -0,0 +1,12 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { version } from "#version"; + +describe("version", () => { + it("equals package.json's version", () => { + const manifest = JSON.parse( + readFileSync(new URL("../package.json", import.meta.url), "utf8"), + ) as { version: string }; + expect(version).toBe(manifest.version); + }); +}); diff --git a/packages/web/docs/core-schemas/info/index.mdx b/packages/web/docs/core-schemas/info/index.mdx index 5da82af60b..6abc56b0bb 100644 --- a/packages/web/docs/core-schemas/info/index.mdx +++ b/packages/web/docs/core-schemas/info/index.mdx @@ -37,6 +37,10 @@ An info object at minimum includes: > {`{ "$schema": "https://ethdebug.github.io/format/schema/info.json", + "ethdebug": { + "schema": "ethdebug/format/info", + "version": "" + }, "compilation": { "id": "compilation-1", "compiler": { "name": "solc", "version": "0.8.20" }, diff --git a/packages/web/docs/core-schemas/info/resources.mdx b/packages/web/docs/core-schemas/info/resources.mdx index 44dce62e91..2e71c1f5f2 100644 --- a/packages/web/docs/core-schemas/info/resources.mdx +++ b/packages/web/docs/core-schemas/info/resources.mdx @@ -130,6 +130,10 @@ keyed by name: title="Shared type definitions" > {`{ + "ethdebug": { + "schema": "ethdebug/format/info/resources", + "version": "" + }, "types": { "struct__Coordinate": { "kind": "struct", diff --git a/packages/web/spec/data/overview.mdx b/packages/web/spec/data/overview.mdx index 3c06afd48e..f22898822c 100644 --- a/packages/web/spec/data/overview.mdx +++ b/packages/web/spec/data/overview.mdx @@ -35,3 +35,4 @@ bar for complete contents. - [Non-negative numeric values](/spec/data/value) - [Hexadecimal strings](/spec/data/hex) - [Unsigned integer](/spec/data/unsigned) +- [Stamp](/spec/data/stamp) diff --git a/packages/web/spec/data/stamp.mdx b/packages/web/spec/data/stamp.mdx new file mode 100644 index 0000000000..ba1d4942e6 --- /dev/null +++ b/packages/web/spec/data/stamp.mdx @@ -0,0 +1,53 @@ +--- +sidebar_position: 4 +--- + +import SchemaViewer from "@site/src/components/SchemaViewer"; + +# Stamp + +:::tip[Summary] + +**ethdebug/format/data/stamp** names the schema an object conforms to +and the version of the specification that defines that schema. An +object carries its stamp in an `ethdebug` field. + +::: + +Each of the three root schemas, **ethdebug/format/program**, +**ethdebug/format/info** and **ethdebug/format/info/resources**, +includes **ethdebug/format/data/stamp** under the key `ethdebug`. The +stamp has this shape: + +```json +{ + "ethdebug": { + "schema": "ethdebug/format/program", + "version": "" + } +} +``` + +`schema` is the name of the schema the object conforms to; each root +schema pins this to its own name. The schema's `$id` is `schema:` +followed by this name. `version` is the version of the specification +that defines `schema`. + + + +## Where the stamp appears + +An **ethdebug/format/info** document and an +**ethdebug/format/info/resources** object **must** carry the stamp. +Both schemas list `ethdebug` in `required`. + +A program inside an info document (at `programs[i]`) **should not** +carry the stamp. The stamp of the info document covers it. + +A program emitted outside an info document **should** carry the stamp. +For example, in solc's standard JSON output, `evm.bytecode.ethdebug` +and `evm.deployedBytecode.ethdebug` are programs that sit beside the +top-level `ethdebug` wrapper, whose `resources` member is the resources +object. Each of these programs should carry its own stamp. + +All objects of one compilation **must** name the same `version`. diff --git a/packages/web/spec/info/info.mdx b/packages/web/spec/info/info.mdx index 797ba6a274..b8638ebfc6 100644 --- a/packages/web/spec/info/info.mdx +++ b/packages/web/spec/info/info.mdx @@ -9,3 +9,9 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Schema + +## Stamp + +This object **must** carry a stamp in its `ethdebug` field that names +**ethdebug/format/info**. A program in `programs` **should not** carry +the stamp. See [Stamp](/spec/data/stamp). diff --git a/packages/web/spec/info/resources.mdx b/packages/web/spec/info/resources.mdx index 5335efbe31..c1aa4fbc67 100644 --- a/packages/web/spec/info/resources.mdx +++ b/packages/web/spec/info/resources.mdx @@ -7,3 +7,10 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Resources lookup schema + +## Stamp + +This object **must** carry a stamp in its `ethdebug` field; see +[Stamp](/spec/data/stamp). + +An info document names **ethdebug/format/info** in its stamp instead. diff --git a/packages/web/spec/overview.mdx b/packages/web/spec/overview.mdx index 44c623c878..db99bff686 100644 --- a/packages/web/spec/overview.mdx +++ b/packages/web/spec/overview.mdx @@ -71,6 +71,12 @@ For the full collection of raw schema listings (in YAML format), please see the [`schemas/` directory](https://github.com/ethdebug/format/tree/main/schemas) in this project's GitHub repository. +A `schema:ethdebug/format/` id names the schema whose source is +`schemas/.schema.yaml` in that repository. The id is an +identifier, not a web address. Emitted data **should not** place it in a +`$schema` key. See [Stamp](/spec/data/stamp) for how objects name the +schema they conform to. + ## Conventions used by this format ### Terminology diff --git a/packages/web/spec/program/program.mdx b/packages/web/spec/program/program.mdx index 86c0403583..15a4cd05a1 100644 --- a/packages/web/spec/program/program.mdx +++ b/packages/web/spec/program/program.mdx @@ -9,3 +9,9 @@ import SchemaViewer from "@site/src/components/SchemaViewer"; # Schema + +## Stamp + +A program emitted outside an info document **should** carry a stamp in +its `ethdebug` field. A program inside an info document **should +not**. See [Stamp](/spec/data/stamp). diff --git a/packages/web/src/components/SchemaViewer.tsx b/packages/web/src/components/SchemaViewer.tsx index 93dd9627b4..31c2c6628d 100644 --- a/packages/web/src/components/SchemaViewer.tsx +++ b/packages/web/src/components/SchemaViewer.tsx @@ -20,7 +20,11 @@ export default function SchemaViewer(props: SchemaViewerProps): JSX.Element { const rootSchemaInfo = describeSchema(props); const { id, rootSchema, yaml: _yaml, pointer } = rootSchemaInfo; - const transformedSchema = transformSchema(rootSchema, id || ""); + // the schema this page shows is outermost in the dynamic scope, so its + // dynamic anchors take precedence over those of any schema it references + const pageAnchors = dynamicAnchors(rootSchema); + + const transformedSchema = transformSchema(rootSchema, id || "", pageAnchors); return ( @@ -39,10 +43,13 @@ export default function SchemaViewer(props: SchemaViewerProps): JSX.Element { schema: { resolve: (uri: URL) => { const id = uri.toString(); - const { schema } = describeSchema({ + const { schema, rootSchema } = describeSchema({ schema: { id }, }); - return transformSchema(schema, id); + return transformSchema(schema, id, { + ...dynamicAnchors(rootSchema), + ...pageAnchors, + }); }, }, }, @@ -97,8 +104,81 @@ export default function SchemaViewer(props: SchemaViewerProps): JSX.Element { ); } -function transformSchema(schema: JSONSchema, id: string): JSONSchema { - return insertIds(ensureRefsLackSiblings(schema), `${id}#`); +type DynamicAnchors = { [name: string]: JSONSchema }; + +function transformSchema( + schema: JSONSchema, + id: string, + anchors: DynamicAnchors, +): JSONSchema { + return insertIds( + ensureRefsLackSiblings(resolveDynamicRefs(schema, anchors)), + `${id}#`, + ); +} + +// collects the schemas in a root schema's `$defs` that declare a +// `$dynamicAnchor`, keyed by anchor name +function dynamicAnchors(rootSchema: JSONSchema): DynamicAnchors { + const anchors: DynamicAnchors = {}; + + const definitions = + (typeof rootSchema === "object" && rootSchema.$defs) || {}; + + for (const definition of Object.values(definitions)) { + if (typeof definition !== "object") { + continue; + } + + const { $dynamicAnchor, ...rest } = definition as { + $dynamicAnchor?: string; + }; + + if (typeof $dynamicAnchor === "string") { + anchors[$dynamicAnchor] = rest as JSONSchema; + } + } + + return anchors; +} + +// recursively replaces each `{ $dynamicRef: "#name" }` with the schema +// that the dynamic anchor `name` resolves to. +// +// docusaurus-json-schema-plugin reports any `$dynamicRef` as an unresolved +// reference, so this integration resolves them itself. The caller supplies +// the anchors in dynamic-scope order: those of the page's own schema +// override those of a schema that the page references. +function resolveDynamicRefs(obj: T, anchors: DynamicAnchors): T { + if (!obj || typeof obj !== "object") { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => resolveDynamicRefs(item, anchors)) as T; + } + + const { $dynamicRef, ...rest } = obj as T & object & { $dynamicRef?: string }; + + const result = Object.entries(rest).reduce((newObj, [key, value]) => { + // @ts-expect-error dynamic key assignment + newObj[key] = resolveDynamicRefs(value, anchors); + return newObj; + }, {} as T); + + if ($dynamicRef === undefined) { + return result; + } + + const anchor = $dynamicRef.startsWith("#") + ? anchors[$dynamicRef.slice(1)] + : undefined; + + if (!anchor) { + throw new Error(`Could not resolve $dynamicRef "${$dynamicRef}"`); + } + + return { ...anchor, ...result }; } function insertIds(obj: T, rootId: string): T { diff --git a/packages/web/src/schemas.ts b/packages/web/src/schemas.ts index 31496043aa..2a82d08f0b 100644 --- a/packages/web/src/schemas.ts +++ b/packages/web/src/schemas.ts @@ -175,7 +175,7 @@ const pointerSchemaIndex: SchemaIndex = { .reduce((a, b) => ({ ...a, ...b }), {}), }; -const dataSchemaIndex: SchemaIndex = ["value", "hex", "unsigned"] +const dataSchemaIndex: SchemaIndex = ["value", "hex", "unsigned", "stamp"] .map((name) => ({ [`schema:ethdebug/format/data/${name}`]: { href: `/spec/data/${name}`, diff --git a/schemas/data/stamp.schema.yaml b/schemas/data/stamp.schema.yaml new file mode 100644 index 0000000000..2eba95e11e --- /dev/null +++ b/schemas/data/stamp.schema.yaml @@ -0,0 +1,41 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "schema:ethdebug/format/data/stamp" + +title: ethdebug/format/data/stamp +description: | + Names the schema an object conforms to and the version of the + specification that defines that schema. + + `schema` is the name of the schema, for example + `ethdebug/format/program`; the `$id` of that schema is `schema:` + followed by this name. `version` is the version of the specification + that defines it, as a semver string. + +type: object + +properties: + schema: + type: string + title: Schema identifier + description: | + The name of the schema this object conforms to, for example + `ethdebug/format/program`. The schema's `$id` is `schema:` + followed by this name. + + version: + type: string + title: Specification version + description: | + The version of the specification that defines + `schema`, as a semver string without build metadata. + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$" + +required: + - schema + - version + +additionalProperties: false + +examples: + - schema: "ethdebug/format/program" + version: "0.1.0-draft.0" diff --git a/schemas/info.schema.yaml b/schemas/info.schema.yaml index e8cef1e175..dc3ac71173 100644 --- a/schemas/info.schema.yaml +++ b/schemas/info.schema.yaml @@ -10,6 +10,21 @@ type: object $ref: "schema:ethdebug/format/info/resources" properties: + ethdebug: + title: Stamp + description: | + Names this schema and the specification version. An info document + must carry this field. A program in `programs` should not carry + one; the stamp of this document covers it. All objects of one + compilation must name the same `version`. + allOf: + - $ref: "schema:ethdebug/format/data/stamp" + # note: whitespace chars are \255 (nbsp) + - title: '{ "schema": "ethdebug/format/info" }' + properties: + schema: + $dynamicRef: "#SchemaName" + programs: type: array items: @@ -19,13 +34,26 @@ properties: $ref: "schema:ethdebug/format/materials/compilation" required: + - ethdebug - compilation - programs unevaluatedProperties: false +$defs: + SchemaName: + $dynamicAnchor: SchemaName + description: | + The schema name that the `ethdebug` field's `schema` must give. + This fills the slot that **ethdebug/format/info/resources** + declares, so that an info document names **ethdebug/format/info**. + const: "ethdebug/format/info" + examples: - - compilation: + - ethdebug: + schema: "ethdebug/format/info" + version: "0.1.0-draft.0" + compilation: id: __301f3b6d85831638 compiler: name: egc diff --git a/schemas/info/resources.schema.yaml b/schemas/info/resources.schema.yaml index 3e26221a50..a4f19da199 100644 --- a/schemas/info/resources.schema.yaml +++ b/schemas/info/resources.schema.yaml @@ -8,6 +8,20 @@ description: | type: object properties: + ethdebug: + title: Stamp + description: | + Names this schema and the specification version. A resources + object must carry this field. All objects of one compilation + must name the same `version`. + allOf: + - $ref: "schema:ethdebug/format/data/stamp" + # note: whitespace chars are \255 (nbsp) + - title: '{ "schema": "ethdebug/format/info/resources" }' + properties: + schema: + $dynamicRef: "#SchemaName" + types: title: Types by name description: | @@ -28,11 +42,24 @@ properties: $ref: "schema:ethdebug/format/materials/compilation" required: + - ethdebug - types - pointers +$defs: + SchemaName: + $dynamicAnchor: SchemaName + description: | + The schema name that the `ethdebug` field's `schema` must give. A + schema that references this one can supply its own name here with + a `SchemaName` dynamic anchor. + const: "ethdebug/format/info/resources" + examples: - - types: + - ethdebug: + schema: "ethdebug/format/info/resources" + version: "0.1.0-draft.0" + types: "struct__Coordinate": kind: struct contains: diff --git a/schemas/program.schema.yaml b/schemas/program.schema.yaml index 889ac832a4..e73aa94861 100644 --- a/schemas/program.schema.yaml +++ b/schemas/program.schema.yaml @@ -8,6 +8,21 @@ description: | type: object properties: + ethdebug: + title: Stamp + description: | + Names this schema and the specification version. A program + emitted outside an info document should carry this field. A + program inside an info document (in `programs`) should not; the + stamp of the info document covers it. + allOf: + - $ref: "schema:ethdebug/format/data/stamp" + # note: whitespace chars are \255 (nbsp) + - title: '{ "schema": "ethdebug/format/program" }' + properties: + schema: + const: "ethdebug/format/program" + compilation: title: Compilation reference by ID description: | @@ -77,6 +92,9 @@ examples: # storedValue += 1; # }; # ``` + ethdebug: + schema: "ethdebug/format/program" + version: "0.1.0-draft.0" contract: name: "Incrementer" definition: