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
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ development topics:
- `docs/src/program/algorithm-index.md` algorithm
documentation

## Algorithm registry

`docs/algorithms/registry.json` is the central mapping
between algorithm specifications (`.tex` pseudocode in
`docs/algorithms/`) and their assembly implementations
(`.s` files in `program/src/dropset/`). Each entry maps
an algorithm name to its `asm` file path. The registry
also contains `syscalls` (Solana runtime syscall URLs)
and `cpis` (cross-program invocation target URLs).

Use the registry when comparing spec vs implementation,
e.g. verifying a `.tex` algorithm matches its `.s` file.

## Key commands

```bash
Expand Down
1 change: 1 addition & 0 deletions cfg/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ bindgen
clippy
clrs
codegen
cpis
dasmac
dropset
dtolnay
Expand Down
29 changes: 27 additions & 2 deletions docs/.vitepress/buildAlgorithmIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// test case files for "// Verifies:" tags. Outputs algorithms/index.json
// with deps, reverse deps, page locations, and associated test cases.
// Algorithm name is the filename stem (used as key and display name).
// The manually maintained registry.json maps each algorithm to its
// assembly implementation and defines external syscall URLs.

import { readFileSync, writeFileSync, readdirSync } from "fs";
import { join, basename, relative } from "path";
Expand All @@ -17,6 +19,9 @@ const CASES_DIR = join(
"cases",
);
const OUTPUT = join(ALGO_DIR, "index.json");
const REGISTRY = JSON.parse(
readFileSync(join(ALGO_DIR, "registry.json"), "utf-8"),
);

// Recursively find all .md files under a directory.
function findMdFiles(dir) {
Expand All @@ -42,10 +47,17 @@ export function buildAlgorithmIndex() {

const calls = new Set();
const syscalls = new Set();
for (const match of code.matchAll(/\\CALL\{([\w-]+)\}/g)) {
const cpis = new Set();
for (const match of code.matchAll(
/\\CALL\{([\w-]+)\}(?:\{([\w-:]+)\})?/g,
)) {
if (match[1] === name) continue;
if (match[1].startsWith("sol-")) {
syscalls.add(match[1].replace(/-/g, "_"));
// Capture CPI target if present (e.g. sol-invoke-signed-c with arg).
if (match[2]) {
cpis.add(match[2].replace(/-/g, "_"));
}
} else {
calls.add(match[1]);
}
Expand All @@ -55,10 +67,23 @@ export function buildAlgorithmIndex() {
page: null,
calls: [...calls],
syscalls: [...syscalls],
cpis: [...cpis],
calledBy: [],
};
}

// Validate registry against .tex files.
const texNames = new Set(Object.keys(index));
const regNames = new Set(Object.keys(REGISTRY.algorithms));
for (const name of texNames) {
if (!regNames.has(name))
throw new Error(`${name}.tex has no entry in registry.json`);
}
for (const name of regNames) {
if (!texNames.has(name))
throw new Error(`registry.json lists "${name}" but no .tex file exists`);
}

// Filter calls to only reference known algorithms (removes notation-only
// names like "Store" that have no .tex definition).
for (const entry of Object.values(index)) {
Expand All @@ -69,7 +94,7 @@ export function buildAlgorithmIndex() {
for (const fullPath of findMdFiles(SRC_DIR)) {
const md = readFileSync(fullPath, "utf-8");
const relPath = relative(SRC_DIR, fullPath);
for (const match of md.matchAll(/<Algorithm\s+tex="([\w-]+)"/g)) {
for (const match of md.matchAll(/<Algorithm\s+id="([\w-]+)"/g)) {
const name = match[1];
if (index[name]) {
// Convert file path to VitePress page path.
Expand Down
91 changes: 71 additions & 20 deletions docs/.vitepress/components/Algorithm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<!-- cspell:word linenum -->
<!-- cspell:word texttt -->
<template>
<!-- Anchor: #algo-ref-<tex> for cross-page and in-page linking. -->
<div :id="`algo-ref-${tex}`">
<!-- Anchor: #algo-ref-<id> for cross-page and in-page linking. -->
<div :id="`algo-ref-${id}`">
<div ref="container" class="pseudocode-container">
<div v-if="asm" ref="asmBlock" class="asm-block"></div>
<div v-if="asmFile" ref="asmBlock" class="asm-block"></div>
<div ref="testsBlock" class="tests-block"></div>
<div v-if="calls.length" class="pseudocode-links pseudocode-links-below">
<div class="pseudocode-link-row">
Expand Down Expand Up @@ -36,15 +36,17 @@
</template>

<script setup>
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import "pseudocode/build/pseudocode.min.css";
import algorithmIndex from "../../algorithms/index.json";
import {
ASM_BASE,
GH_BASE,
GH_ROOT,
asmModules,
registry,
syscallRegistry,
cpiRegistry,
} from "./paths.js";
import { isRestoring } from "../theme/scrollPreserve.js";

Expand All @@ -62,14 +64,16 @@ const testCaseModules = import.meta.glob("../../../tests/tests/cases/*.rs", {
import: "default",
});

// Props: tex is the .tex filename, asm is the optional assembly source file.
// Props: id is the algorithm name (matches .tex filename and registry key).
const props = defineProps({
tex: { type: String, required: true },
asm: { type: String, default: "" },
id: { type: String, required: true },
lineNumber: { type: Boolean, default: true },
lineNumberPunc: { type: String, default: "" },
});

// Resolve assembly file from registry.
const asmFile = computed(() => registry.algorithms[props.id]?.asm || "");

const container = ref(null);
const asmBlock = ref(null);
const testsBlock = ref(null);
Expand Down Expand Up @@ -112,21 +116,27 @@ onMounted(async () => {
const pseudocode = await import("pseudocode");

// Load .tex source at build time via glob import.
const texLoader = texModules[`../../algorithms/${props.tex}.tex`];
if (!texLoader) throw new Error(`Unknown algorithm: ${props.tex}`);
const texLoader = texModules[`../../algorithms/${props.id}.tex`];
if (!texLoader) throw new Error(`Unknown algorithm: ${props.id}`);
const code = await texLoader();

// Resolve forward and reverse deps from the algorithm index.
const entry = algorithmIndex[props.tex];
const entry = algorithmIndex[props.id];
if (entry) {
const syscallLinks = (entry.syscalls || []).map((name) => ({
name,
href: syscallRegistry[name],
external: true,
}));
const cpiLinks = (entry.cpis || []).map((name) => ({
name,
href: cpiRegistry[name],
external: true,
}));
calls.value = [
...resolveLinks(entry.calls, algorithmIndex),
...syscallLinks,
...cpiLinks,
];
calledBy.value = resolveLinks(entry.calledBy, algorithmIndex);
tests.value = entry.tests || [];
Expand All @@ -149,6 +159,19 @@ onMounted(async () => {
);
container.value.insertBefore(rendered, container.value.firstChild);

// Remove the extra indentation pseudocode.js adds to prelude
// comments (those in .ps-block divs directly under .ps-algorithmic
// that contain no .ps-line children).
rendered
.querySelectorAll(".ps-algorithmic > .ps-block")
.forEach((block) => {
if (
!block.querySelector(".ps-line") &&
block.querySelector(".ps-comment")
)
block.style.marginLeft = "0";
});

// Indent comments that precede a block so they align with the
// block's first line rather than the parent control keyword.
rendered.querySelectorAll(".ps-comment").forEach((span) => {
Expand All @@ -169,21 +192,48 @@ onMounted(async () => {

// Turn \CALL{Name} references into clickable links.
// sol-* names are converted to underscore form and linked to the
// external source via syscalls.json; others link to local algorithms.
// external source via the registry; others link to local algorithms.
// When a syscall has a CPI argument (e.g. \CALL{sol-invoke-signed-c}
// {system-program::CreateAccount}), the argument text is replaced
// with a linked CPI target inside parentheses.
//
// pseudocode.js renders both cases as a single text node after the
// funcname span:
// \CALL{f}{} → <span class="ps-funcname">f</span>()
// \CALL{f}{arg} → <span class="ps-funcname">f</span>(arg)
rendered.querySelectorAll(".ps-funcname").forEach((span) => {
const name = span.textContent.trim();
const syscallKey = name.replace(/-/g, "_");
if (syscallRegistry[syscallKey]) {
let next = span.nextSibling;
if (next?.nodeType === Node.TEXT_NODE) {
// Extract the parenthesised content from the text node.
const m = next.textContent.match(/^\(([^)]*)\)/);
if (m) {
const argText = m[1];
// Strip the entire "(...)" from the text node.
next.textContent = next.textContent.slice(m[0].length);
if (argText) {
// CPI target present: convert to underscored display name.
const cpiName = argText.replace(/-/g, "_");
const cpiEl = document.createElement("a");
if (cpiRegistry[cpiName]) {
cpiEl.href = cpiRegistry[cpiName];
cpiEl.target = "_blank";
}
cpiEl.className = "ps-funcname ps-syscall";
cpiEl.textContent = cpiName;
span.after("(", cpiEl, ")");
}
// Empty args: "()" stripped, nothing inserted.
}
}
// Replace the funcname span with a syscall link.
const a = document.createElement("a");
a.href = syscallRegistry[syscallKey];
a.target = "_blank";
a.className = "ps-funcname ps-syscall";
a.textContent = syscallKey;
// Strip the trailing "()" that pseudocode.js emits for \CALL.
let next = span.nextSibling;
if (next?.nodeType === Node.TEXT_NODE) {
next.textContent = next.textContent.replace(/^\(\)/, "");
}
span.replaceWith(a);
} else if (algorithmIndex[name]) {
const a = document.createElement("a");
Expand All @@ -195,9 +245,10 @@ onMounted(async () => {
});

// Load and highlight assembly source if specified.
if (props.asm) {
const asmLoader = asmModules[`${ASM_BASE}${props.asm}.s`];
if (!asmLoader) throw new Error(`Unknown assembly file: ${props.asm}`);
if (asmFile.value) {
const asmLoader = asmModules[`${ASM_BASE}${asmFile.value}.s`];
if (!asmLoader)
throw new Error(`Unknown assembly file: ${asmFile.value}`);
asmCode.value = (await asmLoader()).trimEnd();

const shiki = await import("shiki");
Expand Down Expand Up @@ -248,7 +299,7 @@ onMounted(async () => {

asmBlock.value.innerHTML =
`<details class="details custom-block">` +
`<summary>Implementation: <a href="${GH_BASE}${props.asm}.s" target="_blank">${props.asm}.s</a></summary>` +
`<summary>Implementation: <a href="${GH_BASE}${asmFile.value}.s" target="_blank">${asmFile.value}.s</a></summary>` +
`<div class="language-asm vp-adaptive-theme line-numbers-mode">` +
`<button title="Copy Code" class="copy"></button>` +
`<span class="lang">asm</span>` +
Expand Down
79 changes: 62 additions & 17 deletions docs/.vitepress/components/AlgorithmIndex.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,55 @@
</template>

<script setup>
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import algorithmIndex from "../../algorithms/index.json";
import { syscallRegistry } from "./paths.js";
import { syscallRegistry, cpiRegistry } from "./paths.js";

const props = defineProps({
root: { type: String, default: "" },
});

const chart = ref(null);

// Build algorithm list from the build-time index.
const algorithms = Object.keys(algorithmIndex)
.map((name) => ({
name,
href: `${algorithmIndex[name].page || "/"}#algo-ref-${name}`,
}))
.sort((a, b) => a.name.localeCompare(b.name));
// Collect an algorithm and all its transitive deps (calls only).
function collectDeps(name, index, result = new Set()) {
if (!index[name] || result.has(name)) return result;
result.add(name);
for (const dep of index[name].calls) {
collectDeps(dep, index, result);
}
return result;
}

// Filter the index to a subset when root is specified.
const filteredIndex = computed(() => {
if (!props.root) return algorithmIndex;
const names = collectDeps(props.root, algorithmIndex);
const subset = {};
for (const name of names) {
subset[name] = algorithmIndex[name];
}
return subset;
});

// Build algorithm list from the (possibly filtered) index.
const algorithms = computed(() =>
Object.keys(filteredIndex.value)
.map((name) => ({
name,
href: `${algorithmIndex[name].page || "/"}#algo-ref-${name}`,
}))
.sort((a, b) => a.name.localeCompare(b.name)),
);

// Build a Mermaid graph definition from the algorithm index.
function buildGraph(index) {
const lines = ["graph TD"];
const lines = ["graph LR"];
const syscallNodes = new Set();
const cpiNodes = new Set();
for (const [name, entry] of Object.entries(index)) {
const href = `${entry.page || "/"}#algo-ref-${name}`;
lines.push(` ${name}["${name}"]`);
lines.push(` ${name}["${name}"]:::algo`);
lines.push(` click ${name} "${href}"`);
for (const dep of entry.calls) {
lines.push(` ${name} --> ${dep}`);
Expand All @@ -45,20 +73,37 @@ function buildGraph(index) {
}
lines.push(` ${name} --> ${sc}`);
}
for (const cpi of entry.cpis || []) {
// Mermaid node IDs cannot contain colons, so replace with underscores.
const nodeId = cpi.replace(/::/g, "__");
if (!cpiNodes.has(cpi)) {
cpiNodes.add(cpi);
lines.push(` ${nodeId}(["\`**${cpi}**\`"]):::cpi`);
if (cpiRegistry[cpi]) {
lines.push(` click ${nodeId} "${cpiRegistry[cpi]}" _blank`);
}
}
lines.push(` ${name} --> ${nodeId}`);
}
}
lines.push(
" classDef syscall fill:#e8e8e8,stroke:#999,stroke-dasharray:5 5",
);
lines.push(" classDef algo fill:#d4edda,stroke:#8aba9a");
lines.push(" classDef syscall fill:#e8e8e8,stroke:#999");
lines.push(" classDef cpi fill:#c4d9ed,stroke:#7a9bba");
return lines.join("\n");
}

onMounted(async () => {
try {
// Render Mermaid dep chart.
const mermaid = (await import("mermaid")).default;
mermaid.initialize({ startOnLoad: false, theme: "neutral" });
const graphDef = buildGraph(algorithmIndex);
const { svg } = await mermaid.render("algo-dep-chart", graphDef);
mermaid.initialize({
startOnLoad: false,
theme: "neutral",
flowchart: { nodeSpacing: 20, rankSpacing: 30 },
});
const graphDef = buildGraph(filteredIndex.value);
const chartId = `algo-dep-chart-${props.root || "all"}`;
const { svg } = await mermaid.render(chartId, graphDef);
chart.value.innerHTML = svg;
} catch (e) {
console.error("AlgorithmIndex error:", e);
Expand Down
Loading
Loading