From a3c2fe7064a5ad5b029169a2d319e97a3a4b1b5e Mon Sep 17 00:00:00 2001 From: ClydeWallace22 <48610606+BrandtH22@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:51:17 -0700 Subject: [PATCH] runnable clsp integration - integrates runnable clsp for the academy overview and first clsp lesson --- .../academy/academy-intro/academy-overview.md | 55 +++- docs/academy/chialisp/chialisp-intro.md | 27 ++ package-lock.json | 127 +++++++++ package.json | 4 + src/components/Runnable.tsx | 243 ++++++++++++++++++ src/utils/stringify.ts | 49 ++++ 6 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 src/components/Runnable.tsx create mode 100644 src/utils/stringify.ts diff --git a/docs/academy/academy-intro/academy-overview.md b/docs/academy/academy-intro/academy-overview.md index ce1653469b..591057252a 100644 --- a/docs/academy/academy-intro/academy-overview.md +++ b/docs/academy/academy-intro/academy-overview.md @@ -3,6 +3,8 @@ title: Academy Overview slug: /academy-overview --- +import Runnable from '../../../src/components/Runnable.tsx'; + The lesson pages in Chia Academy are thoughtfully designed to enhance the learning experience for students. Each lesson is organized in a user-friendly and visually appealing manner. The structure typically includes: --- @@ -115,6 +117,57 @@ What is the Chialisp puzzle for squaring a passed argument? --- ## Additional resources -Links to additional reading materials, videos, or external resources may be provided for learners who wish to delve deeper into the lessons subject. +Links to additional reading materials, videos, or external resources may be provided for learners who wish to delve deeper into the lessons subject. + +### Runnable Chialisp and clvm plugins +Runnable plugins are for Chialisp and clvm are provided with all applicable lessons. Take some time to familiarize yourself with the tools and learn how to best make use of them throughout the lessons. +Each plugin has a series of components: + +**Language:** The language of the plugin (Chialisp or clvm) is in the top right corner. +**Solution:** The top section is the input or solution. +**Puzzle:** The bottom section is the puzzle. +**Run:** Each plugin has a play/run button to the right of the language identifier. +**Result:** After clicking run, the result of the puzzle appears below the puzzle. +**Cost:** After clicking run, the clvm cost of the puzzle is calculated and appears in the bottom right corner. +**Errors:** After clicking run, the plugin checks for and provides any errors in place of the result section. + +:::info + +The plugins only validate the formatting and completeness of the code; they do not check for any potential exploits. + +::: + +#### Chialisp plugin +When clicking run, the puzzle will first be serialized into clvm (similar to the `run` command) then the solution will be passed into the serialized puzzle (similar to the `brun` command). +The below example is a Chialisp puzzle that squares the number passed as an argument. + +Note the number `(5)` is used in the solution top section and the Chialisp formatted puzzle is entered in the puzzle bottom section. Clicking run on this puzzle will return `25` as the result. + + + +```chialisp +(mod (arg) + (defun square (number) + (* number number) + ) + (square arg) +) +``` + + + +#### Clvm plugin +When clicking run, the solution will be passed into the serialized puzzle (similar to the `brun` command). +The below example uses the serialized puzzle from above that squares the number passed as an argument. + +Note the number `(5)` is used in the solution top section and the serialized puzzle is entered in the puzzle bottom section. Clicking run on this puzzle will return `25` as the result. + + + +```chialisp +(a (q 2 2 (c 2 (c 5 ()))) (c (q 18 5 5) 1)) +``` + + --- diff --git a/docs/academy/chialisp/chialisp-intro.md b/docs/academy/chialisp/chialisp-intro.md index e1753397c7..60716076bc 100644 --- a/docs/academy/chialisp/chialisp-intro.md +++ b/docs/academy/chialisp/chialisp-intro.md @@ -3,6 +3,8 @@ title: Intro to Chialisp slug: /chialisp-intro --- +import Runnable from '../../../src/components/Runnable.tsx'; + ## Learning objectives - **Syntax and Structure**: Understand the basic Chialisp syntax and structure. - **Puzzles and Solutions**: Understand the use of puzzles and solutions in Chialisp. @@ -169,6 +171,31 @@ What is a Chialisp puzzle that performs the following? ## Additional resources +### Runnable Chialisp and clvm plugins +For information on using these plugins please refer to the [academy overview](/academy-overview#runnable-chialisp-and-clvm-plugins) + +#### Chialisp plugin + + + +```chialisp +(mod (arg1 arg2) + (if (> (+ arg1 arg2) 100) "large" "small") +) +``` + + + +#### Clvm plugin + + + +```chialisp +(a (i 2 (q 1 . "true") (q 1 . "false")) 1) +``` + + + ### Links - General [Chialisp concepts](https://docs.chia.net/guides/chialisp-concepts): overviews of currying, inner puzzles, and morphing conditions. diff --git a/package-lock.json b/package-lock.json index 550970e495..895c2bdd47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,12 @@ "@docusaurus/preset-classic": "^3.0.1", "@easyops-cn/docusaurus-search-local": "^0.38.1", "@mdx-js/react": "^3.0.0", + "clvm-lib": "^1.0.0", + "prism-react-renderer": "^2.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", + "react-simple-code-editor": "^0.13.1", "rehype-katex": "^7.0.0", "remark-math": "^6.0.0" } @@ -4324,6 +4328,14 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, "node_modules/astring": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", @@ -4740,6 +4752,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4799,6 +4828,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -4835,6 +4875,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chia-bls": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chia-bls/-/chia-bls-1.0.1.tgz", + "integrity": "sha512-x2pz0pghxWhvOfVLns+TsszdciGXlweqwaIpvDgS9zTZsX73npqxVHiDcrgtgzTXMYyvZ86JyDLZwDsFLFqNkQ==", + "dependencies": { + "chai": "^4.3.7", + "jssha": "^3.3.0", + "randombytes": "^2.1.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4985,6 +5035,15 @@ "node": ">=6" } }, + "node_modules/clvm-lib": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clvm-lib/-/clvm-lib-1.0.0.tgz", + "integrity": "sha512-9GasKiNVA6cabZT3hQ3O45WcRyVNTdPHEmamjDprVaAbg1+SiD/dqSsP+sDWdbiB+BOx3540w5CrChhliYAGvg==", + "dependencies": { + "chai": "^4.3.7", + "chia-bls": "^1.0.1" + } + }, "node_modules/collapse-white-space": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", @@ -5657,6 +5716,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6848,6 +6918,14 @@ "node": ">=6.9.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -8276,6 +8354,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "engines": { + "node": "*" + } + }, "node_modules/katex": { "version": "0.16.9", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", @@ -8450,6 +8536,14 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -11301,6 +11395,14 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -12395,6 +12497,14 @@ "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-icons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", + "integrity": "sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -12487,6 +12597,15 @@ "react": ">=15" } }, + "node_modules/react-simple-code-editor": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", + "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -14080,6 +14199,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", diff --git a/package.json b/package.json index 9043a71a54..a519bb97c9 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,12 @@ "@docusaurus/preset-classic": "^3.0.1", "@easyops-cn/docusaurus-search-local": "^0.38.1", "@mdx-js/react": "^3.0.0", + "clvm-lib": "^1.0.0", + "prism-react-renderer": "^2.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^4.12.0", + "react-simple-code-editor": "^0.13.1", "rehype-katex": "^7.0.0", "remark-math": "^6.0.0" }, diff --git a/src/components/Runnable.tsx b/src/components/Runnable.tsx new file mode 100644 index 0000000000..13615658bb --- /dev/null +++ b/src/components/Runnable.tsx @@ -0,0 +1,243 @@ +import { useColorMode } from '@docusaurus/theme-common'; +import { Program, ProgramOutput } from 'clvm-lib'; +import { Highlight } from 'prism-react-renderer'; +import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { FaCheck, FaKeyboard, FaPlay, FaTimes } from 'react-icons/fa'; +import Editor from 'react-simple-code-editor'; +import darkTheme from '../theme/prism-dark-theme-chialisp'; +import lightTheme from '../theme/prism-light-theme-chialisp'; +import { onlyText } from '../utils/stringify'; + +export interface RunnableProps { + flavor?: 'clvm' | 'chialisp'; + input?: string; + tests?: Record; + reporter?: React.Dispatch>; +} + +export default function Runnable({ + children, + flavor, + input: initialInput, + tests, + reporter, +}: PropsWithChildren) { + const { colorMode } = useColorMode(); + + const initialCode = useMemo(() => onlyText(children).trim(), []); + const [code, setCode] = useState(initialCode); + + const [input, setInput] = useState( + initialInput ?? Object.keys(tests ?? {})[0]?.trim() ?? '' + ); + const [output, setOutput] = useState(''); + const [cost, setCost] = useState(0n); + const [correct, setCorrect] = useState(null); + + const formatError = (error: string) => error.replace('Error: ', ''); + + const parse = (): Program | null => { + try { + return Program.fromSource(code); + } catch (error) { + setOutput(`While parsing: ${formatError('' + error)}`); + return null; + } + }; + + const compile = (program: Program): Program | null => { + if (!flavor || flavor === 'chialisp') { + try { + return program.compile().value; + } catch (error) { + setOutput(`While compiling: ${formatError('' + error)}`); + return null; + } + } else { + return program; + } + }; + + const evaluate = (program: Program, env: Program): ProgramOutput | null => { + try { + return program.run(env); + } catch (error) { + setOutput(`While evaluating: ${formatError('' + error)}`); + return null; + } + }; + + const run = () => { + const parsed = parse(); + if (!parsed) return; + + const shouldEval = + flavor === 'clvm' || + (parsed.isCons && parsed.first.equals(Program.fromText('mod'))); + + const compiled = compile(parsed); + if (!compiled) return; + + const inputProgram = input ? Program.fromSource(input) : Program.nil; + const outputProgram = shouldEval + ? evaluate(compiled, inputProgram) + : { value: compiled, cost: 0n }; + if (outputProgram) { + setOutput(outputProgram.value.toSource()); + setCost(outputProgram.cost); + } + + let isCorrect = true; + + for (const [testedInput, expectedOutput] of Object.entries(tests ?? {})) { + const inputProgram = Program.fromSource(testedInput); + const outputProgram = shouldEval + ? evaluate(compiled, inputProgram)?.value + : compiled; + + if (!outputProgram || outputProgram.toSource() !== expectedOutput) { + isCorrect = false; + break; + } + } + + reporter?.(isCorrect); + setCorrect(isCorrect); + }; + + const CorrectIcon = correct ? FaCheck : FaTimes; + + // Prevent SSR + const [hydrated, setHydrated] = React.useState(false); + useEffect(() => setHydrated(true), []); + + return ( + + {({ className, style }) => ( +
+          {!input ? (
+            ''
+          ) : (
+            <>
+              
+              
+ + )} + +
+
+ + {!flavor || flavor === 'chialisp' ? 'Chialisp' : 'CLVM'} + + {!input && ( + setInput('()')} + /> + )} + +
+
+ {!output ? ( + '' + ) : ( + <> +
+
+ +
+ {output && ( + <> +
+ +
+ + + )} + + )} +
+ )} +
+ ); +} + +interface HighlightCodeProps { + code: string; + setCode?: React.Dispatch>; + language: string; +} + +function HighlightCode({ code, setCode, language }: HighlightCodeProps) { + const { colorMode } = useColorMode(); + + // Prevent SSR + const [hydrated, setHydrated] = React.useState(false); + useEffect(() => setHydrated(true), []); + + return ( + <> + + {({ tokens, getLineProps, getTokenProps }) => { + let children = tokens.map((line, i) => ( +
+ {line.map((token, key) => ( + + ))} +
+ )); + + return setCode ? ( + setCode(newCode)} + highlight={() => children} + padding={0} + /> + ) : ( + children + ); + }} +
+ + ); +} diff --git a/src/utils/stringify.ts b/src/utils/stringify.ts new file mode 100644 index 0000000000..407df39621 --- /dev/null +++ b/src/utils/stringify.ts @@ -0,0 +1,49 @@ +import { Children, ReactElement, ReactNode, isValidElement } from 'react'; + +export function onlyText(children: ReactNode | ReactNode[]): string { + if (!(children instanceof Array) && !isValidElement(children)) { + return childToString(children); + } + + return Children.toArray(children).reduce( + (text: string, child: ReactNode): string => { + let newText = ''; + + if (isValidElement(child) && hasChildren(child)) { + newText = onlyText(child.props.children); + } else if (isValidElement(child) && !hasChildren(child)) { + newText = ''; + } else { + newText = childToString(child); + } + + return text.concat(newText); + }, + '' + ); +} + +function childToString(child?: ReactNode): string { + if ( + typeof child === 'undefined' || + child === null || + typeof child === 'boolean' + ) { + return ''; + } + + if (JSON.stringify(child) === '{}') { + return ''; + } + + return (child as number | string).toString(); +} + +function hasChildren( + element: ReactNode +): element is ReactElement<{ children: ReactNode | ReactNode[] }> { + return ( + isValidElement<{ children?: ReactNode[] }>(element) && + Boolean(element.props.children) + ); +}