Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package-lock.json

# Auto-generated files
packages/format/src/schemas/yamls.ts
packages/bugc/src/examples/generated.ts

# Solidity fixtures are compiler inputs; this repo has no Solidity Prettier parser.
packages/conformance/test/fixtures/solc/**/*.sol
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default tseslint.config(
"**/*.config.js",
"**/*.config.ts",
"packages/format/src/schemas/yamls.ts",
"packages/bugc/src/examples/generated.ts",
"packages/web/.docusaurus/",
"packages/web/build/",
],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"packages/*"
],
"scripts": {
"build": "yarn --cwd packages/format prepare:yamls && tsc --build packages/format packages/pointers packages/evm packages/bugc packages/conformance packages/programs-react packages/pointers-react",
"build": "yarn --cwd packages/format prepare:yamls && yarn --cwd packages/bugc prepare:examples && tsc --build packages/format packages/pointers packages/evm packages/bugc packages/conformance packages/programs-react packages/pointers-react",
"bundle": "tsx ./bin/bundle-schema.ts",
"test": "vitest",
"test:coverage": "vitest run --coverage",
Expand Down
2 changes: 2 additions & 0 deletions packages/bugc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto-generated from the canonical examples/*.bug files at build time.
src/examples/generated.ts
68 changes: 68 additions & 0 deletions packages/bugc/bin/generate-examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Generates src/examples/generated.ts from the canonical `.bug` example
// files under packages/bugc/examples. Bundlers like webpack (used by the
// docs site) can't glob raw `.bug` files the way Vite can, so we surface
// the sources as an importable module of string literals — the same
// pattern as @ethdebug/format's generated schemas/yamls.ts.

import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const examplesRoot = path.resolve(__dirname, "../examples");

// Walk examplesRoot recursively, collecting `.bug` sources keyed by their
// path relative to examplesRoot (POSIX separators, so keys are stable
// across platforms).
const readExamples = (directory) => {
const sources = {};
const entries = fs.readdirSync(directory, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
Object.assign(sources, readExamples(fullPath));
} else if (entry.isFile() && entry.name.endsWith(".bug")) {
const relativePath = path
.relative(examplesRoot, fullPath)
.split(path.sep)
.join("/");
sources[relativePath] = fs.readFileSync(fullPath, "utf8");
}
}

return sources;
};

// Sort keys so the generated output is deterministic.
const collected = readExamples(examplesRoot);
const exampleSources = {};
for (const key of Object.keys(collected).sort()) {
exampleSources[key] = collected[key];
}

const output = `// 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 type ExampleSourcesByPath = {
[path: string]: string;
};

export const exampleSources: ExampleSourcesByPath = ${JSON.stringify(
exampleSources,
undefined,
2,
)};
`;

const outputDir = path.resolve(__dirname, "../src/examples");
const outputPath = path.join(outputDir, "generated.ts");
const tempPath = outputPath + ".tmp";

fs.mkdirSync(outputDir, { recursive: true });

// Write to a temp file, then rename atomically to avoid race conditions.
fs.writeFileSync(tempPath, output);
fs.renameSync(tempPath, outputPath);
15 changes: 13 additions & 2 deletions packages/bugc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
"type": "module",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./examples": {
"types": "./dist/src/examples/index.d.ts",
"default": "./dist/src/examples/index.js"
}
},
"files": [
"dist",
"bin"
Expand Down Expand Up @@ -54,7 +64,8 @@
"#test/*": "./dist/test/*.js"
},
"scripts": {
"build": "tsc",
"prepare:examples": "node ./bin/generate-examples.js",
"build": "yarn prepare:examples && tsc",
"build:watch": "tsc --watch --preserveWatchOutput",
"watch": "tsc --watch --preserveWatchOutput",
"test": "vitest run",
Expand All @@ -64,7 +75,7 @@
"typecheck": "tsc --noEmit",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"prepare": "tsc"
"prepare": "yarn prepare:examples && tsc"
},
"keywords": [
"ethereum",
Expand Down
106 changes: 106 additions & 0 deletions packages/bugc/src/examples/annotations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, it, expect } from "vitest";

import { stripTestAnnotations } from "./annotations.js";

describe("stripTestAnnotations", () => {
it("removes a /*@test*/ block sitting on its own lines", () => {
const source = [
"create {",
" value = 1;",
" /*@test value-set",
" variables:",
" value:",
" value: 1",
" */",
"}",
"",
].join("\n");

expect(stripTestAnnotations(source)).toBe(
["create {", " value = 1;", "}", ""].join("\n"),
);
});

it("removes a /**@test*/ JSDoc-style block", () => {
const source = [
"create {",
" value = 1;",
" /**@test value-set",
" * variables:",
" * value:",
" * value: 1",
" */",
"}",
"",
].join("\n");

expect(stripTestAnnotations(source)).toBe(
["create {", " value = 1;", "}", ""].join("\n"),
);
});

it("collapses the blank lines left around removed blocks", () => {
const source = [
" lastSender = msg.sender;",
"",
" /*@test a",
" variables: { x: 1 }",
" */",
"",
" /*@test b",
" variables: { y: 2 }",
" */",
" return;",
"",
].join("\n");

expect(stripTestAnnotations(source)).toBe(
[" lastSender = msg.sender;", "", " return;", ""].join("\n"),
);
});

it("strips // @wip, @skip, and @expect-* directive lines", () => {
const source = [
"// @wip",
"// @skip needs work",
"// @expect-parse-error",
"// @expect-bytecode-error",
"name Thing;",
"",
].join("\n");

expect(stripTestAnnotations(source)).toBe(["name Thing;", ""].join("\n"));
});

it("leaves ordinary // comments and code untouched", () => {
const source = [
"code {",
" // a normal comment",
" x = 1; // trailing comment",
"}",
"",
].join("\n");

expect(stripTestAnnotations(source)).toBe(source);
});

it("does not touch block comments that are not @test", () => {
const source = ["/* just a comment */", "name Thing;", ""].join("\n");

expect(stripTestAnnotations(source)).toBe(source);
});

it("normalizes to a single trailing newline and no leading blanks", () => {
const source = ["", "", "name Thing;", "", "", ""].join("\n");

expect(stripTestAnnotations(source)).toBe(["name Thing;", ""].join("\n"));
});

it("removes an inline @test block but keeps the code before it", () => {
const source = [" x = 1; /*@test t\n variables: {}\n */", ""].join(
"\n",
);

expect(stripTestAnnotations(source)).toBe([" x = 1;", ""].join("\n"));
});
});
38 changes: 38 additions & 0 deletions packages/bugc/src/examples/annotations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* The canonical `.bug` example files double as bugc's behavioral test
* fixtures, so they carry inline test metadata: `/*@test … *\/` YAML
* blocks and `// @wip` / `// @skip` / `// @expect-*` directive lines.
* That metadata is noise when the same source is shown in an editor.
*
* `stripTestAnnotations` removes it, leaving clean, display-ready BUG
* source. The canonical files stay the single source of truth; consumers
* that render examples (the playground, the docs widget) strip on the way
* out.
*/

// Matches a `/*@test … *\/` or `/**@test … *\/` block, together with any
// indentation on the line it opens and the trailing newline — so a block
// alone on its line disappears without leaving a blank behind.
const TEST_BLOCK = /[ \t]*\/\*\*?@test\b[\s\S]*?\*\/[ \t]*\n?/g;

// Matches a whole-line `// @wip` / `// @skip …` / `// @expect-*` directive.
const DIRECTIVE_LINE =
/^[ \t]*\/\/[ \t]*@(?:wip|skip|expect-[a-z-]+)\b.*(?:\n|$)/gm;

/**
* Strip bugc test annotations from BUG source, yielding display-ready text.
*/
export function stripTestAnnotations(source: string): string {
const stripped = source
.replace(TEST_BLOCK, "")
.replace(DIRECTIVE_LINE, "")
// Trim whitespace an inline removal may have left at a line's end.
.replace(/[ \t]+$/gm, "")
// Collapse the blank runs that removals leave behind to one blank line.
.replace(/\n{3,}/g, "\n\n")
// No leading blank lines; end with exactly one trailing newline.
.replace(/^\n+/, "")
.replace(/\s+$/, "");

return stripped === "" ? "" : stripped + "\n";
}
79 changes: 79 additions & 0 deletions packages/bugc/src/examples/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* These tests guard the example subpath export: that the generated source
* map is populated, and — crucially — that stripping the test annotations
* is purely cosmetic. Because the strip only removes comments, a stripped
* example must compile to byte-identical bytecode as its raw counterpart;
* if it doesn't, the strip has eaten real code.
*/
import { describe, it, expect } from "vitest";

import { compile } from "#compiler";
import { exampleSources, examplePaths, stripTestAnnotations } from "./index.js";

// A representative set of examples that compile cleanly. Kept small so the
// test stays fast; the point is to exercise strip/compile equivalence, not
// to re-test the whole example corpus (test/examples covers that).
const COMPILABLE = [
"basic/minimal.bug",
"basic/functions.bug",
"basic/conditionals.bug",
"basic/array-length.bug",
"intermediate/arrays.bug",
"intermediate/mappings.bug",
];

const hex = (bytes?: Uint8Array): string =>
bytes ? Buffer.from(bytes).toString("hex") : "";

describe("example sources", () => {
it("surfaces the canonical .bug files keyed by relative path", () => {
expect(examplePaths.length).toBeGreaterThan(0);
expect(examplePaths).toEqual(Object.keys(exampleSources));
expect(exampleSources["basic/minimal.bug"]).toContain("name Minimal;");
});

it("keeps the raw sources' test annotations intact", () => {
// The raw map is the test fixtures verbatim — annotations included.
const anyHasTestBlock = examplePaths.some((p) =>
exampleSources[p].includes("@test"),
);
expect(anyHasTestBlock).toBe(true);
});
});

describe("stripTestAnnotations on real examples", () => {
for (const path of COMPILABLE) {
it(`yields annotation-free source for ${path}`, () => {
const clean = stripTestAnnotations(exampleSources[path]);
expect(clean).not.toContain("@test");
expect(clean).not.toMatch(/\/\/\s*@(?:wip|skip|expect-)/);
});

it(`compiles ${path} to the same bytecode raw and stripped`, async () => {
const raw = exampleSources[path];
const clean = stripTestAnnotations(raw);

const rawResult = await compile({
to: "bytecode",
source: raw,
optimizer: { level: 0 },
});
const cleanResult = await compile({
to: "bytecode",
source: clean,
optimizer: { level: 0 },
});

expect(rawResult.success).toBe(true);
expect(cleanResult.success).toBe(true);
if (!rawResult.success || !cleanResult.success) return;

expect(hex(cleanResult.value.bytecode.runtime)).toBe(
hex(rawResult.value.bytecode.runtime),
);
expect(hex(cleanResult.value.bytecode.create)).toBe(
hex(rawResult.value.bytecode.create),
);
});
}
});
30 changes: 30 additions & 0 deletions packages/bugc/src/examples/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Canonical BUG example sources, surfaced for editors and playgrounds.
*
* The `.bug` files under `packages/bugc/examples` are the single source of
* truth: bugc's behavioral tests read them from disk, and this module
* exposes the same sources as an importable string map so bundlers (webpack,
* Vite) can ship them without globbing the filesystem.
*
* The raw sources carry bugc's test annotations (`/*@test … *\/` blocks and
* `// @wip` / `// @skip` / `// @expect-*` directives). Call
* {@link stripTestAnnotations} to get display-ready source for an editor.
* Which examples to show, and how to label them, is left to each consumer.
*/

import { exampleSources } from "./generated.js";

export { exampleSources } from "./generated.js";
export type { ExampleSourcesByPath } from "./generated.js";
export { stripTestAnnotations } from "./annotations.js";

/** Relative paths of every canonical example (e.g. `"basic/minimal.bug"`). */
export const examplePaths: string[] = Object.keys(exampleSources);

/** A single BUG example: its canonical path and raw source. */
export interface BugExample {
/** Path relative to `packages/bugc/examples` (e.g. `"basic/minimal.bug"`). */
path: string;
/** Raw source, including bugc's inline test annotations. */
source: string;
}
Loading