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
44 changes: 44 additions & 0 deletions packages/bugc-react/src/examples.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* The curated example set must stay small, clean, and — above all —
* compilable: these strings are shipped verbatim into the docs
* playground editor, so a typo would surface as a broken example.
*/
import { describe, it, expect } from "vitest";
import { compile } from "@ethdebug/bugc";
import { bugExamples } from "./examples.js";

describe("bugExamples", () => {
it("is the curated trio (counter, functions, arrays)", () => {
expect(bugExamples.map((e) => e.name)).toEqual([
"counter",
"functions",
"arrays",
]);
});

it("gives every example a display name and non-empty source", () => {
for (const ex of bugExamples) {
expect(ex.displayName.trim().length).toBeGreaterThan(0);
expect(ex.code.trim().length).toBeGreaterThan(0);
}
});

it("carries no leftover @test annotation blocks", () => {
for (const ex of bugExamples) {
expect(ex.code).not.toContain("@test");
}
});

// Each curated source must compile cleanly to bytecode — this is
// the guard that matters, since these ship straight to the editor.
for (const ex of bugExamples) {
it(`compiles ${ex.name} to bytecode without errors`, async () => {
const result = await compile({
to: "bytecode",
source: ex.code,
optimizer: { level: 0 },
});
expect(result.success).toBe(true);
});
}
});
102 changes: 102 additions & 0 deletions packages/bugc-react/src/examples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* A small, curated set of BUG programs for the docs playground's
* example selector. Kept intentionally short and clean — these
* strings are shown verbatim in the editor, so they carry no
* `/*@test*\/`-style behavioral annotations (unlike the raw
* `packages/bugc/examples` sources they're distilled from).
*
* The set is validated by examples.test.ts: every entry must
* compile to bytecode without errors.
*/

/** A named, display-labelled BUG source for the example selector. */
export interface BugExample {
/** Stable identifier (used as the <option> value). */
name: string;
/** Human-readable label shown in the dropdown. */
displayName: string;
/** The BUG source. */
code: string;
}

const counter = `name Counter;

storage {
[0] count: uint256;
[1] threshold: uint256;
}

create {
count = 0;
threshold = 100;
}

code {
// Increment the counter, wrapping when it hits the threshold
count = count + 1;
if (count >= threshold) {
count = 0;
}
}
`;

const functions = `name Functions;

define {
// A leaf helper
function add(a: uint256, b: uint256) -> uint256 {
return a + b;
};

// Calls add() twice — a nested function call
function addThree(x: uint256, y: uint256, z: uint256) -> uint256 {
let partial = add(x, y);
return add(partial, z);
};
}

storage {
[0] result: uint256;
}

code {
result = addThree(10, 20, 30);
}
`;

const arrays = `name Arrays;

storage {
[0] numbers: array<uint256, 5>;
[1] sum: uint256;
[2] max: uint256;
}

code {
// Fill the array with squares: 0, 1, 4, 9, 16
for (let i = 0; i < 5; i = i + 1) {
numbers[i] = i * i;
}

// Sum every element
sum = 0;
for (let i = 0; i < 5; i = i + 1) {
sum = sum + numbers[i];
}

// Track the largest element
max = numbers[0];
for (let i = 1; i < 5; i = i + 1) {
if (numbers[i] > max) {
max = numbers[i];
}
}
}
`;

/** The curated example set, in display order. */
export const bugExamples: BugExample[] = [
{ name: "counter", displayName: "Counter", code: counter },
{ name: "functions", displayName: "Function calls", code: functions },
{ name: "arrays", displayName: "Arrays & loops", code: arrays },
];
3 changes: 3 additions & 0 deletions packages/bugc-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export {
type EditorSourceRange,
} from "#components/Editor";

// Curated example programs (for playground selectors)
export { bugExamples, type BugExample } from "./examples.js";

// Utilities
export {
// Debug utilities
Expand Down
13 changes: 13 additions & 0 deletions packages/bugc-react/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true,
environment: "jsdom",
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "dist/", "**/*.test.ts", "**/*.test.tsx"],
},
},
});
6 changes: 4 additions & 2 deletions packages/web/src/theme/BugcExample/BugPlayground.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@
align-items: center;
}

.bug-playground-opt-control {
.bug-playground-opt-control,
.bug-playground-example-control {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}

.bug-playground-opt-control select {
.bug-playground-opt-control select,
.bug-playground-example-control select {
padding: 0.25rem 0.5rem;
border-radius: var(--ifm-global-radius);
border: 1px solid var(--ifm-color-emphasis-300);
Expand Down
90 changes: 62 additions & 28 deletions packages/web/src/theme/BugcExample/BugPlayground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
IrView,
CfgView,
BytecodeView,
bugExamples,
type BugExample,
type SourceRange,
type BytecodeOutput,
} from "@ethdebug/bugc-react";
Expand Down Expand Up @@ -129,8 +131,20 @@ async function compile(
type TabType = "ast" | "ir" | "cfg" | "bytecode";

export interface BugPlaygroundProps {
/** Initial code to display in the editor */
/**
* Initial code to display in the editor. When provided, the
* example selector is hidden by default (the embed is showing
* one specific program); when omitted, the playground opens on
* the first curated example and shows the selector.
*/
initialCode?: string;
/** Curated examples offered in the selector dropdown. */
examples?: BugExample[];
/**
* Force the example selector on or off. Defaults to showing it
* only when `initialCode` is not provided.
*/
showExampleSelector?: boolean;
/** Default optimization level (0-3) */
defaultOptimizationLevel?: number;
/** Whether to show optimization level selector */
Expand All @@ -143,33 +157,20 @@ export interface BugPlaygroundProps {
* Interactive BUG compiler playground.
*/
export function BugPlayground({
initialCode = `name Counter;

storage {
[0] count: uint256;
[1] threshold: uint256;
}

create {
count = 0;
threshold = 100;
}

code {
// Increment the counter
count = count + 1;

// Check threshold
if (count >= threshold) {
count = 0;
}
}
`,
initialCode,
examples = bugExamples,
showExampleSelector,
defaultOptimizationLevel = 3,
showOptimizationSelector = true,
height = "600px",
}: BugPlaygroundProps): JSX.Element {
const [code, setCode] = useState(initialCode);
// Show the selector only when the embed hasn't pinned a specific
// program via initialCode (unless explicitly overridden).
const showSelector =
showExampleSelector ?? (initialCode === undefined && examples.length > 0);

const [selectedExample, setSelectedExample] = useState(examples[0]?.name);
const [code, setCode] = useState(initialCode ?? examples[0]?.code ?? "");
const [optimizationLevel, setOptimizationLevel] = useState(
defaultOptimizationLevel,
);
Expand All @@ -185,10 +186,12 @@ code {
setHighlightedRanges(ranges);
}, []);

const handleCompile = useCallback(async () => {
// Compile an explicit source (so example switches recompile the
// just-selected program, not the stale `code` from closure).
const runCompile = useCallback(async (src: string, level: number) => {
setIsCompiling(true);
try {
const result = await compile(code, optimizationLevel);
const result = await compile(src, level);
setCompileResult(result);
if (!result.success) {
setActiveTab("ast"); // Show AST tab for errors
Expand All @@ -202,11 +205,27 @@ code {
} finally {
setIsCompiling(false);
}
}, [code, optimizationLevel]);
}, []);

const handleCompile = useCallback(() => {
runCompile(code, optimizationLevel);
}, [runCompile, code, optimizationLevel]);

const handleExampleChange = useCallback(
(name: string) => {
const example = examples.find((e) => e.name === name);
if (!example) return;
setSelectedExample(name);
setCode(example.code);
runCompile(example.code, optimizationLevel);
},
[examples, optimizationLevel, runCompile],
);

// Compile on mount
useEffect(() => {
handleCompile();
runCompile(code, optimizationLevel);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const tabs: { id: TabType; label: string }[] = [
Expand All @@ -220,6 +239,21 @@ code {
<div className="bug-playground" style={{ height }}>
<div className="bug-playground-header">
<div className="bug-playground-controls">
{showSelector && (
<label className="bug-playground-example-control">
<span>Example:</span>
<select
value={selectedExample}
onChange={(e) => handleExampleChange(e.target.value)}
>
{examples.map((example) => (
<option key={example.name} value={example.name}>
{example.displayName}
</option>
))}
</select>
</label>
)}
{showOptimizationSelector && (
<label className="bug-playground-opt-control">
<span>Optimization:</span>
Expand Down
Loading