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
49 changes: 49 additions & 0 deletions .claude/skills/pr-title-description/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: pr-title-description
description: Write or update a PR title and description for the current branch, matching the style of recent PRs.
disable-model-invocation: true
user-invocable: true
---

<!-- cspell:word oneline -->

# `pr-title-description`

Write (or update) the title and description
for the pull request on the current branch.

<!-- markdownlint-disable MD041 -->

## Steps

<!-- markdownlint-enable MD041 -->

1. Identify the current branch and its PR
(if one exists) using
`gh pr list --head <branch>`.

1. Get the full diff against `main`:
`git diff main..HEAD` and
`git log main..HEAD --oneline`.

1. Fetch the body of the 3 most recent merged
PRs to match their style:

```sh
gh pr list --state merged --limit 3 \
--json number,title,body
```

1. Write a concise PR description that mirrors
the format and tone of those recent PRs.
Typically this means a `# Changes` section
with a numbered list. Add a `# Background`
section only if the changes need non-obvious
context.

1. If a PR already exists for the branch, update
it with `gh pr edit <number> --body "..."`.
Otherwise, report the description so the user
can create the PR.

1. Show the user the PR URL when done.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
.DS_Store
deploy
target
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[workspace]
members = []

[workspace.dependencies]
mollusk-svm = "0.10.3"
pinocchio = "0.10.2"

[workspace.package]
edition = "2024"
version = "0.1.0"
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
# cspell:word vite
.PHONY: all
.PHONY: asm
.PHONY: clean
.PHONY: test

all: docs-prettier pre-commit-lint
clean:
test:

# Assemble the program.
asm:
sbpf build --arch v3 --deploy-dir deploy

# Build and serve docs locally for development.
docs-dev:
cd docs \
cd docs && npm install \
&& rm -rf .vitepress/cache .vitepress/dist node_modules/.vite \
&& npx vitepress dev
&& npx vitepress dev --open
# Format docs with Prettier.
docs-prettier:
cd docs && npx prettier --write .
cd docs && npm install && npx prettier --write .
# Build and serve docs locally in production mode.
docs-prod:
cd docs \
&& rm -rf .vitepress/cache .vitepress/dist node_modules/.vite \
&& npm ci \
&& npx vitepress build \
&& npx vitepress preview
&& npx vitepress preview --open
# Run pre-commit lint checks on all files.
pre-commit-lint:
pre-commit run --config cfg/pre-commit/lint.yml --all-files
3 changes: 3 additions & 0 deletions cfg/dictionary.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
dasmac
dropset
insn
katex
ldxb
ldxdw
sbpf
solana
vitepress
5 changes: 4 additions & 1 deletion cfg/pre-commit/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ repos:
repo: 'https://github.com/checkmake/checkmake.git'
rev: 'v0.3.2'
- hooks:
- id: 'chktex'
- args:
- '-n1' # Allow command terminated with space, e.g. `\STATE foo`.
- '-n8' # Allow standard dash in ALGORITHM-NAME.
id: 'chktex'
repo: 'https://github.com/randolf-scholz/latex-hooks'
rev: 'v0.1.24'
...
2 changes: 1 addition & 1 deletion docs/.vitepress/buildAlgorithmIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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+src="([\w-]+)"/g)) {
for (const match of md.matchAll(/<Algorithm\s+tex="([\w-]+)"/g)) {
const name = match[1];
if (index[name]) {
// Convert file path to VitePress page path.
Expand Down
136 changes: 115 additions & 21 deletions docs/.vitepress/components/Algorithm.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
<!-- cspell:word funcname -->
<!-- cspell:word linenum -->
<!-- cspell:word punc -->
<!-- cspell:word shiki -->
<!-- cspell:word texttt -->
<template>
<!-- Anchor: #algo-<src> for cross-page and in-page linking. -->
<div :id="`algo-${src}`" ref="container" class="pseudocode-container">
<div v-if="calls.length" class="pseudocode-links pseudocode-links-below">
<div class="pseudocode-link-row">
Calls:
<a v-for="dep in calls" :key="dep.name" :href="dep.href">
{{ dep.name }}
</a>
<!-- Anchor: #algorithm-<tex> for cross-page and in-page linking. -->
<div :id="`algorithm-${tex}`">
<div ref="container" class="pseudocode-container">
<div v-if="asm" ref="asmBlock" class="asm-block"></div>
<div v-if="calls.length" class="pseudocode-links pseudocode-links-below">
<div class="pseudocode-link-row">
Calls:
<a v-for="dep in calls" :key="dep.name" :href="dep.href">
{{ dep.name }}
</a>
</div>
</div>
</div>
<div v-if="calledBy.length" class="pseudocode-links pseudocode-links-below">
<div class="pseudocode-link-row">
Called by:
<a v-for="dep in calledBy" :key="dep.name" :href="dep.href">
{{ dep.name }}
</a>
<div
v-if="calledBy.length"
class="pseudocode-links pseudocode-links-below"
>
<div class="pseudocode-link-row">
Called by:
<a v-for="dep in calledBy" :key="dep.name" :href="dep.href">
{{ dep.name }}
</a>
</div>
</div>
</div>
</div>
Expand All @@ -35,22 +42,31 @@ const texModules = import.meta.glob("../../algorithms/*.tex", {
import: "default",
});

// Import all .s files at build time via Vite's glob import with ?raw.
const asmModules = import.meta.glob("../../../src/dropset/**/*.s", {
query: "?raw",
import: "default",
});

// src is the .tex filename, rest are pseudocode.js options.
const props = defineProps({
src: { type: String, required: true },
tex: { type: String, required: true },
asm: { type: String, default: "" },
lineNumber: { type: Boolean, default: true },
lineNumberPunc: { type: String, default: "" },
});

const container = ref(null);
const asmBlock = ref(null);
const calls = ref([]);
const calledBy = ref([]);
const asmCode = ref("");

// Resolve a list of algorithm names to links using the algorithm index.
function resolveLinks(names, index) {
return names.map((name) => {
const page = index[name]?.page || "";
const href = `${page}#algo-${name}`;
const href = `${page}#algorithm-${name}`;
return { name, href };
});
}
Expand All @@ -63,12 +79,12 @@ onMounted(async () => {
const pseudocode = await import("pseudocode");

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

// Resolve forward and reverse deps from the algorithm index.
const entry = algorithmIndex[props.src];
const entry = algorithmIndex[props.tex];
if (entry) {
calls.value = resolveLinks(entry.calls, algorithmIndex);
calledBy.value = resolveLinks(entry.calledBy, algorithmIndex);
Expand Down Expand Up @@ -101,12 +117,83 @@ onMounted(async () => {
const name = span.textContent.trim();
if (algorithmIndex[name]) {
const a = document.createElement("a");
a.href = `${algorithmIndex[name].page || "/"}#algo-${name}`;
a.href = `${algorithmIndex[name].page || "/"}#algorithm-${name}`;
a.className = "ps-funcname";
a.textContent = span.textContent;
span.replaceWith(a);
}
});

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

const shiki = await import("shiki");
const highlighter = await shiki.createHighlighter({
themes: ["github-dark", "github-light"],
langs: ["asm"],
});

// Shiki's asm grammar misclassifies indented "# if" as a preprocessor
// directive. Fix comment lines in the token stream after highlighting.
const commentColor = { dark: "#6A737D", light: "#6A737D" };
const fixComments = {
tokens(lines) {
const src = asmCode.value.split("\n");
for (let i = 0; i < lines.length; i++) {
if (src[i]?.trimStart().startsWith("#")) {
const text = lines[i].map((t) => t.content).join("");
lines[i] = [
{
content: text,
color: commentColor.dark,
htmlStyle: `--shiki-dark:${commentColor.dark};--shiki-light:${commentColor.light}`,
},
];
}
}
},
};

const highlighted = highlighter.codeToHtml(asmCode.value, {
lang: "asm",
themes: { dark: "github-dark", light: "github-light" },
defaultColor: false,
transformers: [fixComments],
});

// Build line numbers.
const lineCount = asmCode.value.split("\n").length;
const lineNumsHtml = Array.from(
{ length: lineCount },
(_, i) => `<span class="line-number">${i + 1}</span><br>`,
).join("");

// Produce the exact HTML VitePress would for :::details + ```asm```.
const pre = highlighted
.replace("<pre ", '<pre tabindex="0" ')
.replace(/class="shiki/, 'class="shiki vp-code');

asmBlock.value.innerHTML =
`<details class="details custom-block">` +
`<summary>Implementation</summary>` +
`<div class="language-asm vp-adaptive-theme line-numbers-mode">` +
`<button title="Copy Code" class="copy"></button>` +
`<span class="lang">asm</span>` +
pre +
`<div class="line-numbers-wrapper" aria-hidden="true">${lineNumsHtml}</div>` +
`</div>` +
`</details>`;

// Wire up copy button.
asmBlock.value.querySelector(".copy").addEventListener("click", () => {
navigator.clipboard.writeText(asmCode.value);
});

highlighter.dispose();
}
} catch (e) {
console.error("Pseudocode render error:", e);
container.value.textContent = "Error: " + e.message;
Expand Down Expand Up @@ -183,4 +270,11 @@ onMounted(async () => {
margin-right: 0.2em;
color: var(--vp-c-text-2);
}

/* Implementation details block inside the algorithm container. */
.asm-block {
margin-top: 0.75em;
border-top: 1px solid var(--vp-c-divider);
padding-top: 0.5em;
}
</style>
4 changes: 2 additions & 2 deletions docs/.vitepress/components/AlgorithmIndex.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ const chart = ref(null);
const algorithms = Object.keys(algorithmIndex)
.map((name) => ({
name,
href: `${algorithmIndex[name].page || "/"}#algo-${name}`,
href: `${algorithmIndex[name].page || "/"}#algorithm-${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"];
for (const [name, entry] of Object.entries(index)) {
const href = `${entry.page || "/"}#algo-${name}`;
const href = `${entry.page || "/"}#algorithm-${name}`;
lines.push(` ${name}["${name}"]`);
lines.push(` click ${name} "${href}"`);
for (const dep of entry.calls) {
Expand Down
14 changes: 7 additions & 7 deletions docs/algorithms/ENTRYPOINT.tex
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
\COMMENT{Input buffer.}
\INPUT $r_1 = input$
\COMMENT{Instruction data.}
\INPUT $r_2 = data$
\PROCEDURE{ENTRYPOINT}{$input, data$}
\STATE $data_l = data.length$
\STATE $d_d = data.discriminator$
\IF{$d_d$ == \texttt{Discriminator::RegisterMarket}}
\RETURN \CALL{REGISTER-MARKET}{$input$, $data$, $data_l$}
\INPUT $r_2 = insn$
\PROCEDURE{ENTRYPOINT}{$input, insn$}
\STATE $insn\_len = insn.length$
\STATE $insn\_d = insn.discriminant$
\IF{$insn\_d$ == \texttt{Discriminant::RegisterMarket}}
\RETURN \CALL{REGISTER-MARKET}{$input$, $insn$, $insn\_len$}
\ENDIF
\RETURN \texttt{Error::InvalidInstructionDiscriminator}
\RETURN \texttt{Error::InvalidDiscriminant}
\ENDPROCEDURE
\end{algorithmic}
\end{algorithm}
12 changes: 6 additions & 6 deletions docs/algorithms/REGISTER-MARKET.tex
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
\COMMENT{Input buffer.}
\INPUT $r_1 = input$
\COMMENT{Instruction data.}
\INPUT $r_2 = data$
\INPUT $r_2 = insn$
\COMMENT{Instruction data length.}
\INPUT $r_3 = data_l$
\REQUIRE $data.discriminator == \texttt{Discriminator::RegisterMarket}$
\PROCEDURE{REGISTER-MARKET}{$input$, $data$, $data\_l$}
\IF{$data_l \neq$ \texttt{InstructionDataLength::RegisterMarket}}
\RETURN \texttt{Error::InvalidInstructionDataLength}
\INPUT $r_3 = insn\_len$
\REQUIRE $insn.discriminant == \texttt{Discriminant::RegisterMarket}$
\PROCEDURE{REGISTER-MARKET}{$input, insn, insn\_len$}
\IF{$insn\_len \neq$ \texttt{InstructionLength::RegisterMarket}}
\RETURN \texttt{Error::InvalidInstructionLength}
\ENDIF
\ENDPROCEDURE
\end{algorithmic}
Expand Down
3 changes: 2 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ check out the [Solana Opcode Guide].
## About this site

This site is built in [Vitepress], and leverages custom [Vue components] to
provide formal [CLRS]-style algorithmic specifications via [pseudocode.js].
provide formal [CLRS]-style algorithmic specifications via [pseudocode.js], with
collapsible SBPF assembly implementations sourced directly from the codebase.

The auto-generated [algorithm index](indices/algorithms) contains a
[mermaid]-style dependency chart of all algorithms, which are additionally
Expand Down
Loading
Loading