From 186f0ee3d276fc6223a7eeb9c64ce34651559b59 Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Sat, 1 Mar 2025 14:48:50 +0800 Subject: [PATCH 01/12] (feat) [skip ci] integrate with js-slang stepper/rewrite --- .../content/SideContentSubstVisualizer.tsx | 271 ++++++++-------- .../content/_SideContentSubstVisualizer.tsx | 291 ++++++++++++++++++ src/pages/playground/PlaygroundTabs.tsx | 4 +- src/styles/_workspace.scss | 18 +- 4 files changed, 433 insertions(+), 151 deletions(-) create mode 100644 src/commons/sideContent/content/_SideContentSubstVisualizer.tsx diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 71378f488e..77a464d4a9 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,16 +1,29 @@ import 'js-slang/dist/editors/ace/theme/source'; -import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { + Button, + ButtonGroup, + Callout, + Card, + Classes, + Divider, + Popover, + Pre, + Slider +} from '@blueprintjs/core'; import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; -import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; import React, { useCallback, useEffect, useState } from 'react'; -import AceEditor from 'react-ace'; import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; +import { IStepperPropContents, redex } from 'js-slang/dist/stepper/stepperV2'; +import { StepperExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression'; +import { StepperLiteral } from 'js-slang/dist/stepper/stepperV2/nodes/Literal'; +import { StepperUnaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/UnaryExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/BinaryExpression'; const SubstDefaultText = () => { return ( @@ -57,24 +70,24 @@ const SubstCodeDisplay = (props: { content: string }) => { ); }; -type SubstVisualizerProps = { +type SubstVisualizerPropsAST = { content: IStepperPropContents[]; workspaceLocation: SideContentLocation; }; -const SideContentSubstVisualizer: React.FC = props => { +const SideContentSubstVisualizer: React.FC = props => { const [stepValue, setStepValue] = useState(1); const lastStepValue = props.content.length; - const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component - + const hasRunCode = lastStepValue !== 0; const dispatch = useDispatch(); const alertSideContent = useCallback( () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), [props.workspaceLocation, dispatch] ); - + console.log(props) // set source mode as 2 useEffect(() => { + HighlightRulesSelector(2); ModeSelector(2); }, []); @@ -87,49 +100,6 @@ const SideContentSubstVisualizer: React.FC = props => { } }, [props.content, setStepValue, alertSideContent]); - // Stepper function call helpers - const getPreviousFunctionCall = useCallback( - (value: number) => { - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = props.content[contIndex]?.function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex - 1; i > -1; i--) { - const previousFunction = props.content[i].function; - if (previousFunction !== undefined && currentFunction === previousFunction) { - return i + 1; - } - } - return null; - }, - [lastStepValue, props.content] - ); - - const getNextFunctionCall = useCallback( - (value: number) => { - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = props.content[contIndex]?.function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex + 1; i < props.content.length; i++) { - const nextFunction = props.content[i].function; - if (nextFunction !== undefined && currentFunction === nextFunction) { - return i + 1; - } - } - return null; - }, - [lastStepValue, props.content] - ); - - // Stepper handlers - const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; - const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; - const stepPreviousFunctionCall = () => - setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); - const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); const stepFirst = () => setStepValue(1); const stepLast = () => setStepValue(lastStepValue); const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); @@ -151,66 +121,18 @@ const SideContentSubstVisualizer: React.FC = props => { ]; const hotkeyHandler = getHotkeyHandler(hotkeyBindings); - // Rendering helpers - const getText = useCallback( - (value: number) => { + const getExplanation = useCallback( + (value: number): string => { const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex; - const split = pathified.code.split('@redex'); - if (split.length > 1) { - let text = split[0]; - for (let i = 1; i < split.length; i++) { - text = text + redex + split[i]; - } - return text; - } else { - return redexed; - } + return props.content[contIndex][2]; }, [lastStepValue, props.content] ); - const getDiffMarkers = useCallback( - (value: number) => { + const getAST = useCallback( + (value: number): IStepperPropContents => { const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex.split('\n'); - - const diffMarkers = [] as any[]; - if (redex.length > 0) { - const mainprog = redexed.split('@redex'); - let text = mainprog[0]; - let front = text.split('\n'); - - let startR = front.length - 1; - let startC = front[startR].length; - - for (let i = 0; i < mainprog.length - 1; i++) { - const endR = startR + redex.length - 1; - const endC = - redex.length === 1 - ? startC + redex[redex.length - 1].length - : redex[redex.length - 1].length; - - diffMarkers.push({ - startRow: startR, - startCol: startC, - endRow: endR, - endCol: endC, - className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', - type: 'background' - }); - - text = text + redex + mainprog[i + 1]; - front = text.split('\n'); - startR = front.length - 1; - startC = front[startR].length; - } - } - return diffMarkers; + return props.content[contIndex]; }, [lastStepValue, props.content] ); @@ -230,11 +152,7 @@ const SideContentSubstVisualizer: React.FC = props => { />
-
{' '}
- {hasRunCode ? ( - - ) : ( - - )} - {hasRunCode ? ( - - ) : null} + {hasRunCode ? : } + {hasRunCode ? : null} +
+
Expression stepper
+
{'Double arrows << and >> are replaced with stepFirst and stepLast.'}
+
); }; -export default SideContentSubstVisualizer; + +// custom AST renderer +function StepperDisplayer(props: IStepperPropContents) { + const getNodeType = useCallback( + (node: StepperExpression): string => { + if (props[1] === node) { + return "beforeMarker"; + } + if (redex.postRedex === node) { + return "afterMarker"; + } + return ""; + }, + [props] + ); + + const convertNode = useCallback( + (node: StepperExpression): React.ReactNode => { + const convertors = { + Literal(node: StepperLiteral) { + return {node.value}; + }, + UnaryExpression(node: StepperUnaryExpression) { + return ( + + {` ${node.operator}`} + {convertNode(node.argument)} + + ); + }, + BinaryExpression(node: StepperBinaryExpression) { + return ( + + {convertNode(node.left)} + {` ${node.operator} `} + {convertNode(node.right)} + + ); + } + }; + const convertor = convertors[node.type]; + // @ts-expect-error node actually has type StepperExpression + const converted = convertor(node); + if (getNodeType(node) === "") { + return {converted}; + } else { + return ( + + + +
+ {"Contraction rule "} + {getNodeType(node) === "beforeMarker" ? "E1 -> E2" : "finished"} +
+ +
+                          {JSON.stringify(node, null, 4)}
+                        
+ } + > + +
+
+ + } + > + {converted} +
+
+ ); + } + }, + [props, getNodeType] + ); + + const getConvertedNode = useCallback((): React.ReactNode => { + return convertNode(props[0]); + }, [props, convertNode]); + + return
{getConvertedNode()}
; +} + +export default SideContentSubstVisualizer; \ No newline at end of file diff --git a/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx new file mode 100644 index 0000000000..71378f488e --- /dev/null +++ b/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx @@ -0,0 +1,291 @@ +import 'js-slang/dist/editors/ace/theme/source'; + +import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; +import classNames from 'classnames'; +import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; +import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; +import React, { useCallback, useEffect, useState } from 'react'; +import AceEditor from 'react-ace'; +import { useDispatch } from 'react-redux'; + +import { beginAlertSideContent } from '../SideContentActions'; +import { SideContentLocation, SideContentType } from '../SideContentTypes'; + +const SubstDefaultText = () => { + return ( +
+
+ Welcome to the Stepper! +
+
+ On this tab, the REPL will be hidden from view, so do check that your code has no errors + before running the stepper. You may use this tool by writing your program on the left, then + dragging the slider above to see its evaluation. +
+
+ On even-numbered steps, the part of the program that will be evaluated next is highlighted + in yellow. On odd-numbered steps, the result of the evaluation is highlighted in green. You + can change the maximum steps limit (500-5000, default 1000) in the control bar. +
+
+ + Some useful keyboard shortcuts: +
+
+ a: Move to the first step +
+ e: Move to the last step +
+ f: Move to the next step +
+ b: Move to the previous step +
+
+ Note that these shortcuts are only active when the browser focus is on this tab (click on or + above the explanation text). +
+
+ ); +}; + +const SubstCodeDisplay = (props: { content: string }) => { + return ( + +
{props.content}
+
+ ); +}; + +type SubstVisualizerProps = { + content: IStepperPropContents[]; + workspaceLocation: SideContentLocation; +}; + +const SideContentSubstVisualizer: React.FC = props => { + const [stepValue, setStepValue] = useState(1); + const lastStepValue = props.content.length; + const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component + + const dispatch = useDispatch(); + const alertSideContent = useCallback( + () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), + [props.workspaceLocation, dispatch] + ); + + // set source mode as 2 + useEffect(() => { + HighlightRulesSelector(2); + ModeSelector(2); + }, []); + + // reset stepValue when content changes + useEffect(() => { + setStepValue(1); + if (props.content.length > 0) { + alertSideContent(); + } + }, [props.content, setStepValue, alertSideContent]); + + // Stepper function call helpers + const getPreviousFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex - 1; i > -1; i--) { + const previousFunction = props.content[i].function; + if (previousFunction !== undefined && currentFunction === previousFunction) { + return i + 1; + } + } + return null; + }, + [lastStepValue, props.content] + ); + + const getNextFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex + 1; i < props.content.length; i++) { + const nextFunction = props.content[i].function; + if (nextFunction !== undefined && currentFunction === nextFunction) { + return i + 1; + } + } + return null; + }, + [lastStepValue, props.content] + ); + + // Stepper handlers + const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; + const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; + const stepPreviousFunctionCall = () => + setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); + const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); + const stepFirst = () => setStepValue(1); + const stepLast = () => setStepValue(lastStepValue); + const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); + const stepNext = () => setStepValue(Math.min(props.content.length, stepValue + 1)); + + // Setup hotkey bindings + const hotkeyBindings: HotkeyItem[] = hasRunCode + ? [ + ['a', stepFirst], + ['f', stepNext], + ['b', stepPrevious], + ['e', stepLast] + ] + : [ + ['a', () => {}], + ['f', () => {}], + ['b', () => {}], + ['e', () => {}] + ]; + const hotkeyHandler = getHotkeyHandler(hotkeyBindings); + + // Rendering helpers + const getText = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex; + const split = pathified.code.split('@redex'); + if (split.length > 1) { + let text = split[0]; + for (let i = 1; i < split.length; i++) { + text = text + redex + split[i]; + } + return text; + } else { + return redexed; + } + }, + [lastStepValue, props.content] + ); + + const getDiffMarkers = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex.split('\n'); + + const diffMarkers = [] as any[]; + if (redex.length > 0) { + const mainprog = redexed.split('@redex'); + let text = mainprog[0]; + let front = text.split('\n'); + + let startR = front.length - 1; + let startC = front[startR].length; + + for (let i = 0; i < mainprog.length - 1; i++) { + const endR = startR + redex.length - 1; + const endC = + redex.length === 1 + ? startC + redex[redex.length - 1].length + : redex[redex.length - 1].length; + + diffMarkers.push({ + startRow: startR, + startCol: startC, + endRow: endR, + endCol: endC, + className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', + type: 'background' + }); + + text = text + redex + mainprog[i + 1]; + front = text.split('\n'); + startR = front.length - 1; + startC = front[startR].length; + } + } + return diffMarkers; + }, + [lastStepValue, props.content] + ); + + return ( +
+ +
+ +
{' '} +
+ {hasRunCode ? ( + + ) : ( + + )} + {hasRunCode ? ( + + ) : null} +
+ ); +}; + +export default SideContentSubstVisualizer; diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index 6a726b4035..6684f52358 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -49,8 +49,8 @@ export const makeSubstVisualizerTabFrom = ( editorOutput && editorOutput.type === 'result' && editorOutput.value instanceof Array && - editorOutput.value[0] === Object(editorOutput.value[0]) && - isStepperOutput(editorOutput.value[0]) + editorOutput.value[0] === Object(editorOutput.value[0]) + // && isStepperOutput(editorOutput.value[0]) // FIX: implement isStepperOutput ) { return editorOutput.value; } else { diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 63af80872f..19f8a55829 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -418,18 +418,32 @@ $code-color-notification: #f9f0d7; margin: 15px; height: unset; + .stepper-display { + font-family: 'Inconsolata', 'Consolas', monospace; + } + .beforeMarker { background: rgba(179, 101, 57, 0.75); - position: absolute; + pointer-events: auto; + cursor: pointer; z-index: 20; } + .beforeMarker:hover { + background: rgba(100, 101, 57, 0.75); + } + .afterMarker { background: green; - position: absolute; + pointer-events: auto; + cursor: pointer; z-index: 20; } + .afterMarker:hover { + background: rgba(61, 101, 57, 0.75); + } + .#{$ns}-slider-label { width: -webkit-max-content; width: -moz-max-content; From d506417f98d65e29c2254519d46f98cc019088fb Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Sat, 1 Mar 2025 14:48:50 +0800 Subject: [PATCH 02/12] (feat) [skip ci] integrate with js-slang stepper/rewrite --- .../content/SideContentSubstVisualizer.tsx | 280 ++++++++--------- .../content/_SideContentSubstVisualizer.tsx | 291 ++++++++++++++++++ src/pages/playground/PlaygroundTabs.tsx | 5 +- src/styles/_workspace.scss | 26 +- 4 files changed, 451 insertions(+), 151 deletions(-) create mode 100644 src/commons/sideContent/content/_SideContentSubstVisualizer.tsx diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 71378f488e..4fe5aad2a7 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,16 +1,30 @@ import 'js-slang/dist/editors/ace/theme/source'; -import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { + Button, + ButtonGroup, + Callout, + Card, + Classes, + Divider, + Popover, + Pre, + Slider +} from '@blueprintjs/core'; import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; -import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; import React, { useCallback, useEffect, useState } from 'react'; -import AceEditor from 'react-ace'; import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; +import { IStepperPropContents, toStringWithMarker } from 'js-slang/dist/stepper/stepperV2'; +import { StepperExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression'; +import { StepperLiteral } from 'js-slang/dist/stepper/stepperV2/nodes/Literal'; +import { StepperUnaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/UnaryExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/BinaryExpression'; +import { generate } from 'astring'; const SubstDefaultText = () => { return ( @@ -57,22 +71,20 @@ const SubstCodeDisplay = (props: { content: string }) => { ); }; -type SubstVisualizerProps = { +type SubstVisualizerPropsAST = { content: IStepperPropContents[]; workspaceLocation: SideContentLocation; }; -const SideContentSubstVisualizer: React.FC = props => { +const SideContentSubstVisualizer: React.FC = props => { const [stepValue, setStepValue] = useState(1); const lastStepValue = props.content.length; - const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component - + const hasRunCode = lastStepValue !== 0; const dispatch = useDispatch(); const alertSideContent = useCallback( () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), [props.workspaceLocation, dispatch] ); - // set source mode as 2 useEffect(() => { HighlightRulesSelector(2); @@ -87,49 +99,6 @@ const SideContentSubstVisualizer: React.FC = props => { } }, [props.content, setStepValue, alertSideContent]); - // Stepper function call helpers - const getPreviousFunctionCall = useCallback( - (value: number) => { - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = props.content[contIndex]?.function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex - 1; i > -1; i--) { - const previousFunction = props.content[i].function; - if (previousFunction !== undefined && currentFunction === previousFunction) { - return i + 1; - } - } - return null; - }, - [lastStepValue, props.content] - ); - - const getNextFunctionCall = useCallback( - (value: number) => { - const contIndex = value <= lastStepValue ? value - 1 : 0; - const currentFunction = props.content[contIndex]?.function; - if (currentFunction === undefined) { - return null; - } - for (let i = contIndex + 1; i < props.content.length; i++) { - const nextFunction = props.content[i].function; - if (nextFunction !== undefined && currentFunction === nextFunction) { - return i + 1; - } - } - return null; - }, - [lastStepValue, props.content] - ); - - // Stepper handlers - const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; - const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; - const stepPreviousFunctionCall = () => - setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); - const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); const stepFirst = () => setStepValue(1); const stepLast = () => setStepValue(lastStepValue); const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); @@ -151,66 +120,18 @@ const SideContentSubstVisualizer: React.FC = props => { ]; const hotkeyHandler = getHotkeyHandler(hotkeyBindings); - // Rendering helpers - const getText = useCallback( - (value: number) => { + const getExplanation = useCallback( + (value: number): string => { const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex; - const split = pathified.code.split('@redex'); - if (split.length > 1) { - let text = split[0]; - for (let i = 1; i < split.length; i++) { - text = text + redex + split[i]; - } - return text; - } else { - return redexed; - } + return props.content[contIndex][2]; }, [lastStepValue, props.content] ); - const getDiffMarkers = useCallback( - (value: number) => { + const getAST = useCallback( + (value: number): IStepperPropContents => { const contIndex = value <= lastStepValue ? value - 1 : 0; - const pathified = props.content[contIndex]; - const redexed = pathified.code; - const redex = pathified.redex.split('\n'); - - const diffMarkers = [] as any[]; - if (redex.length > 0) { - const mainprog = redexed.split('@redex'); - let text = mainprog[0]; - let front = text.split('\n'); - - let startR = front.length - 1; - let startC = front[startR].length; - - for (let i = 0; i < mainprog.length - 1; i++) { - const endR = startR + redex.length - 1; - const endC = - redex.length === 1 - ? startC + redex[redex.length - 1].length - : redex[redex.length - 1].length; - - diffMarkers.push({ - startRow: startR, - startCol: startC, - endRow: endR, - endCol: endC, - className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', - type: 'background' - }); - - text = text + redex + mainprog[i + 1]; - front = text.split('\n'); - startR = front.length - 1; - startC = front[startR].length; - } - } - return diffMarkers; + return props.content[contIndex]; }, [lastStepValue, props.content] ); @@ -230,11 +151,7 @@ const SideContentSubstVisualizer: React.FC = props => { />
-
{' '}
- {hasRunCode ? ( - - ) : ( - - )} - {hasRunCode ? ( - - ) : null} + {hasRunCode ? : } + {hasRunCode ? : null} +
+
Expression stepper
+
{'Double arrows << and >> are replaced with stepFirst and stepLast.'}
+
); }; +/////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// +// Iterative solution: get marked position for custom markers +function CustomASTRenderer(props: IStepperPropContents) { + const getStringWithMarker = useCallback(() => { + return toStringWithMarker(props); + }, [props]); + return ( +
+ {getStringWithMarker().map(content => ( + {content['text']} + ))} +
+ ); +} + +/* +function StepperDisplayer(props: IStepperPropContents) { + const getNodeType = useCallback( + (node: StepperExpression): string => { + if (props[1] === node && props[2] === 'before') { + return 'beforeMarker'; + } + if (props[1] === node && props[2] === 'after') { + return 'afterMarker'; + } + return ''; + }, + [props] + ); + + // TODO: Move this logic from frontend to js-slang + const convertNode = useCallback( + (node: StepperExpression): React.ReactNode => { + const convertors = { + Literal(node: StepperLiteral) { + return {node.value}; + }, + UnaryExpression(node: StepperUnaryExpression) { + return ( + + {` ${node.operator}`} + {convertNode(node.argument)} + + ); + }, + BinaryExpression(node: StepperBinaryExpression) { + return ( + + {convertNode(node.left)} + {` ${node.operator} `} + {convertNode(node.right)} + + ); + } + }; + const convertor = convertors[node.type]; + // @ts-expect-error node actually has type StepperExpression + const converted = convertor(node); + if (getNodeType(node) === '') { + return {converted}; + } else { + return ( + + + +
+ {'Contraction rule '} + + {getNodeType(node) === 'beforeMarker' ? 'E1 -> E2' : 'finished'} + +
+
+ + } + > + {converted} +
+
+ ); + } + }, + [props, getNodeType] + ); + + const getConvertedNode = useCallback((): React.ReactNode => { + return convertNode(props[0]); + }, [props, convertNode]); + + return
{getConvertedNode()}
; +} +*/ export default SideContentSubstVisualizer; diff --git a/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx new file mode 100644 index 0000000000..71378f488e --- /dev/null +++ b/src/commons/sideContent/content/_SideContentSubstVisualizer.tsx @@ -0,0 +1,291 @@ +import 'js-slang/dist/editors/ace/theme/source'; + +import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; +import classNames from 'classnames'; +import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; +import { IStepperPropContents } from 'js-slang/dist/stepper/stepper'; +import React, { useCallback, useEffect, useState } from 'react'; +import AceEditor from 'react-ace'; +import { useDispatch } from 'react-redux'; + +import { beginAlertSideContent } from '../SideContentActions'; +import { SideContentLocation, SideContentType } from '../SideContentTypes'; + +const SubstDefaultText = () => { + return ( +
+
+ Welcome to the Stepper! +
+
+ On this tab, the REPL will be hidden from view, so do check that your code has no errors + before running the stepper. You may use this tool by writing your program on the left, then + dragging the slider above to see its evaluation. +
+
+ On even-numbered steps, the part of the program that will be evaluated next is highlighted + in yellow. On odd-numbered steps, the result of the evaluation is highlighted in green. You + can change the maximum steps limit (500-5000, default 1000) in the control bar. +
+
+ + Some useful keyboard shortcuts: +
+
+ a: Move to the first step +
+ e: Move to the last step +
+ f: Move to the next step +
+ b: Move to the previous step +
+
+ Note that these shortcuts are only active when the browser focus is on this tab (click on or + above the explanation text). +
+
+ ); +}; + +const SubstCodeDisplay = (props: { content: string }) => { + return ( + +
{props.content}
+
+ ); +}; + +type SubstVisualizerProps = { + content: IStepperPropContents[]; + workspaceLocation: SideContentLocation; +}; + +const SideContentSubstVisualizer: React.FC = props => { + const [stepValue, setStepValue] = useState(1); + const lastStepValue = props.content.length; + const hasRunCode = lastStepValue !== 0; // 'content' property is initialised to '[]' by Playground component + + const dispatch = useDispatch(); + const alertSideContent = useCallback( + () => dispatch(beginAlertSideContent(SideContentType.substVisualizer, props.workspaceLocation)), + [props.workspaceLocation, dispatch] + ); + + // set source mode as 2 + useEffect(() => { + HighlightRulesSelector(2); + ModeSelector(2); + }, []); + + // reset stepValue when content changes + useEffect(() => { + setStepValue(1); + if (props.content.length > 0) { + alertSideContent(); + } + }, [props.content, setStepValue, alertSideContent]); + + // Stepper function call helpers + const getPreviousFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex - 1; i > -1; i--) { + const previousFunction = props.content[i].function; + if (previousFunction !== undefined && currentFunction === previousFunction) { + return i + 1; + } + } + return null; + }, + [lastStepValue, props.content] + ); + + const getNextFunctionCall = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const currentFunction = props.content[contIndex]?.function; + if (currentFunction === undefined) { + return null; + } + for (let i = contIndex + 1; i < props.content.length; i++) { + const nextFunction = props.content[i].function; + if (nextFunction !== undefined && currentFunction === nextFunction) { + return i + 1; + } + } + return null; + }, + [lastStepValue, props.content] + ); + + // Stepper handlers + const hasPreviousFunctionCall = getPreviousFunctionCall(stepValue) !== null; + const hasNextFunctionCall = getNextFunctionCall(stepValue) !== null; + const stepPreviousFunctionCall = () => + setStepValue(getPreviousFunctionCall(stepValue) ?? stepValue); + const stepNextFunctionCall = () => setStepValue(getNextFunctionCall(stepValue) ?? stepValue); + const stepFirst = () => setStepValue(1); + const stepLast = () => setStepValue(lastStepValue); + const stepPrevious = () => setStepValue(Math.max(1, stepValue - 1)); + const stepNext = () => setStepValue(Math.min(props.content.length, stepValue + 1)); + + // Setup hotkey bindings + const hotkeyBindings: HotkeyItem[] = hasRunCode + ? [ + ['a', stepFirst], + ['f', stepNext], + ['b', stepPrevious], + ['e', stepLast] + ] + : [ + ['a', () => {}], + ['f', () => {}], + ['b', () => {}], + ['e', () => {}] + ]; + const hotkeyHandler = getHotkeyHandler(hotkeyBindings); + + // Rendering helpers + const getText = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex; + const split = pathified.code.split('@redex'); + if (split.length > 1) { + let text = split[0]; + for (let i = 1; i < split.length; i++) { + text = text + redex + split[i]; + } + return text; + } else { + return redexed; + } + }, + [lastStepValue, props.content] + ); + + const getDiffMarkers = useCallback( + (value: number) => { + const contIndex = value <= lastStepValue ? value - 1 : 0; + const pathified = props.content[contIndex]; + const redexed = pathified.code; + const redex = pathified.redex.split('\n'); + + const diffMarkers = [] as any[]; + if (redex.length > 0) { + const mainprog = redexed.split('@redex'); + let text = mainprog[0]; + let front = text.split('\n'); + + let startR = front.length - 1; + let startC = front[startR].length; + + for (let i = 0; i < mainprog.length - 1; i++) { + const endR = startR + redex.length - 1; + const endC = + redex.length === 1 + ? startC + redex[redex.length - 1].length + : redex[redex.length - 1].length; + + diffMarkers.push({ + startRow: startR, + startCol: startC, + endRow: endR, + endCol: endC, + className: value % 2 === 0 ? 'beforeMarker' : 'afterMarker', + type: 'background' + }); + + text = text + redex + mainprog[i + 1]; + front = text.split('\n'); + startR = front.length - 1; + startC = front[startR].length; + } + } + return diffMarkers; + }, + [lastStepValue, props.content] + ); + + return ( +
+ +
+ +
{' '} +
+ {hasRunCode ? ( + + ) : ( + + )} + {hasRunCode ? ( + + ) : null} +
+ ); +}; + +export default SideContentSubstVisualizer; diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index 6a726b4035..5245fcd156 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -1,5 +1,4 @@ import { IconNames } from '@blueprintjs/icons'; -import { isStepperOutput } from 'js-slang/dist/stepper/stepper'; import { InterpreterOutput } from 'src/commons/application/ApplicationTypes'; import Markdown from 'src/commons/Markdown'; import SideContentRemoteExecution from 'src/commons/sideContent/content/remoteExecution/SideContentRemoteExecution'; @@ -49,8 +48,8 @@ export const makeSubstVisualizerTabFrom = ( editorOutput && editorOutput.type === 'result' && editorOutput.value instanceof Array && - editorOutput.value[0] === Object(editorOutput.value[0]) && - isStepperOutput(editorOutput.value[0]) + editorOutput.value[0] === Object(editorOutput.value[0]) + // && isStepperOutput(editorOutput.value[0]) // FIX: implement isStepperOutput ) { return editorOutput.value; } else { diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 63af80872f..1c45d91bc3 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -418,18 +418,40 @@ $code-color-notification: #f9f0d7; margin: 15px; height: unset; + .stepper-display { + font-family: 'Inconsolata', 'Consolas', monospace; + } + + .stepper-literal { + color:#FF6078; + } + + .stepper-operator { + color: #F89210; + } + .beforeMarker { background: rgba(179, 101, 57, 0.75); - position: absolute; + pointer-events: auto; + cursor: pointer; z-index: 20; } + .beforeMarker:hover { + background: rgba(100, 101, 57, 0.75); + } + .afterMarker { background: green; - position: absolute; + pointer-events: auto; + cursor: pointer; z-index: 20; } + .afterMarker:hover { + background: rgba(61, 101, 57, 0.75); + } + .#{$ns}-slider-label { width: -webkit-max-content; width: -moz-max-content; From e6c992a7aba679312fb8bc9dcef59eead0c6bac5 Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Fri, 14 Mar 2025 23:25:42 +0800 Subject: [PATCH 03/12] (feat) add custom ast renderer from scratch --- .../content/SideContentSubstVisualizer.tsx | 225 +++++++++--------- src/styles/_workspace.scss | 4 + 2 files changed, 123 insertions(+), 106 deletions(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 3c8f1b6c8b..b25d5073bb 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,16 +1,6 @@ import 'js-slang/dist/editors/ace/theme/source'; -import { - Button, - ButtonGroup, - Callout, - Card, - Classes, - Divider, - Popover, - Pre, - Slider -} from '@blueprintjs/core'; +import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; @@ -19,7 +9,19 @@ import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; -import { IStepperPropContents, toStringWithMarker } from 'js-slang/dist/stepper/stepperV2'; +import { IStepperPropContents } from 'js-slang/dist/stepper/stepperV2'; +import { StepperBaseNode } from 'js-slang/dist/stepper/stepperV2/interface'; +import { StepperLiteral } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/Literal'; +import { StepperUnaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/UnaryExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/BinaryExpression'; +import { StepperProgram } from 'js-slang/dist/stepper/stepperV2/nodes/Program'; +import { StepperBlockStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/BlockStatement'; +import { StepperIdentifier } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/Identifier'; +import { StepperExpressionStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/ExpressionStatement'; +import { + StepperVariableDeclaration, + StepperVariableDeclarator +} from 'js-slang/dist/stepper/stepperV2/nodes/Statement/VariableDeclaration'; const SubstDefaultText = () => { return ( @@ -72,6 +74,7 @@ type SubstVisualizerPropsAST = { }; const SideContentSubstVisualizer: React.FC = props => { + console.log(props); const [stepValue, setStepValue] = useState(1); const lastStepValue = props.content.length; const hasRunCode = lastStepValue !== 0; @@ -82,7 +85,6 @@ const SideContentSubstVisualizer: React.FC = props => { ); // set source mode as 2 useEffect(() => { - HighlightRulesSelector(2); ModeSelector(2); }, []); @@ -119,7 +121,13 @@ const SideContentSubstVisualizer: React.FC = props => { const getExplanation = useCallback( (value: number): string => { const contIndex = value <= lastStepValue ? value - 1 : 0; - return props.content[contIndex][2]; + // Right now, prioritize the first marker + const markers = props.content[contIndex].markers; + if (markers === undefined) { + return '...'; + } else { + return markers[0].explanation ?? '...'; + } }, [lastStepValue, props.content] ); @@ -173,104 +181,109 @@ const SideContentSubstVisualizer: React.FC = props => { }; /////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// -// Iterative solution: get marked position for custom markers -// Normally, this will be handled using ACEEditor -function CustomASTRenderer(props: IStepperPropContents) { - const getStringWithMarker = useCallback(() => { - return toStringWithMarker(props); - }, [props]); - return ( -
- {getStringWithMarker().map(content => ( - {content['text']} - ))} -
- ); -} - -/* -function StepperDisplayer(props: IStepperPropContents) { - const getNodeType = useCallback( - (node: StepperExpression): string => { - if (props[1] === node && props[2] === 'before') { - return 'beforeMarker'; +function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { + function renderNode(node: StepperBaseNode): React.ReactNode { + const wrapMarkerStyle = (renderNode: React.ReactNode): React.ReactNode => { + if (props.markers === undefined) { + return renderNode; } - if (props[1] === node && props[2] === 'after') { - return 'afterMarker'; - } - return ''; - }, - [props] - ); - - // TODO: Move this logic from frontend to js-slang - const convertNode = useCallback( - (node: StepperExpression): React.ReactNode => { - const convertors = { - Literal(node: StepperLiteral) { - return {node.value}; - }, - UnaryExpression(node: StepperUnaryExpression) { - return ( - - {` ${node.operator}`} - {convertNode(node.argument)} - - ); - }, - BinaryExpression(node: StepperBinaryExpression) { - return ( - - {convertNode(node.left)} - {` ${node.operator} `} - {convertNode(node.right)} - - ); + var returnNode = {renderNode}; + props.markers.forEach(marker => { + if (marker.redex === node) { + returnNode = {returnNode}; } - }; - const convertor = convertors[node.type]; - // @ts-expect-error node actually has type StepperExpression - const converted = convertor(node); - if (getNodeType(node) === '') { - return {converted}; - } else { + }); + return returnNode; + }; + + const renderers = { + Literal(node: StepperLiteral) { + return {node.value}; + }, + Identifier(node: StepperIdentifier) { + return {node.name}; + }, + UnaryExpression(node: StepperUnaryExpression) { return ( - - - -
- {'Contraction rule '} - - {getNodeType(node) === 'beforeMarker' ? 'E1 -> E2' : 'finished'} - -
-
- - } - > - {converted} -
+ + {` ${node.operator}`} + {renderNode(node.argument)} + + ); + }, + BinaryExpression(node: StepperBinaryExpression) { + // TODO: check precedence + return ( + + {renderNode(node.left)} + {` ${node.operator} `} + {renderNode(node.right)} + + ); + }, + Program(node: StepperProgram) { + return ( + + {node.body.map(ast => ( +
{renderNode(ast)}
+ ))} +
+ ); + }, + BlockStatement(node: StepperBlockStatement) { + return ( + + {'{'} + {node.body.map(ast => ( +
+ {renderNode(ast)} +
+ ))} + {'}'} +
+ ); + }, + ExpressionStatement(node: StepperExpressionStatement) { + return ( + + {renderNode(node.expression)} + {';'} + + ); + }, + VariableDeclaration(node: StepperVariableDeclaration) { + return ( + + {node.kind} + {node.declarations.map((ast, idx) => ( + + {idx !== 0 && ', '} + {renderNode(ast)} + + ))} + {';'} + + ); + }, + VariableDeclarator(node: StepperVariableDeclarator) { + return ( + + {renderNode(node.id)} + {' = '} + {node.init ? renderNode(node.init) : 'undefined'} ); } - }, - [props, getNodeType] - ); - - const getConvertedNode = useCallback((): React.ReactNode => { - return convertNode(props[0]); - }, [props, convertNode]); + }; - return
{getConvertedNode()}
; + // @ts-ignore + const renderer = renderers[node.type]; + return renderer ? wrapMarkerStyle(renderer(node)) : '...'; + } + const getDisplayedNode = useCallback((): React.ReactNode => { + return renderNode(props.ast); + }, [props]); + return
{getDisplayedNode()}
; } -*/ + export default SideContentSubstVisualizer; diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index d22d6b71a0..ef505fd985 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -431,6 +431,10 @@ $code-color-notification: #f9f0d7; color: #F89210; } + .stepper-identifier { + color: yellow; + } + .stepper-display { font-family: 'Inconsolata', 'Consolas', monospace; } From 29941265f6e8dcf91ee819e8c3b7cb77e920929c Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Fri, 14 Mar 2025 23:25:42 +0800 Subject: [PATCH 04/12] (feat) add custom ast renderer from scratch --- .../content/SideContentSubstVisualizer.tsx | 225 +++++++++--------- src/styles/_workspace.scss | 4 + 2 files changed, 123 insertions(+), 106 deletions(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 3c8f1b6c8b..ba77111516 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,16 +1,6 @@ import 'js-slang/dist/editors/ace/theme/source'; -import { - Button, - ButtonGroup, - Callout, - Card, - Classes, - Divider, - Popover, - Pre, - Slider -} from '@blueprintjs/core'; +import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; @@ -19,7 +9,19 @@ import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; -import { IStepperPropContents, toStringWithMarker } from 'js-slang/dist/stepper/stepperV2'; +import { IStepperPropContents } from 'js-slang/dist/stepper/stepperV2'; +import { StepperBaseNode } from 'js-slang/dist/stepper/stepperV2/interface'; +import { StepperLiteral } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/Literal'; +import { StepperUnaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/UnaryExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/BinaryExpression'; +import { StepperProgram } from 'js-slang/dist/stepper/stepperV2/nodes/Program'; +import { StepperBlockStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/BlockStatement'; +import { StepperIdentifier } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/Identifier'; +import { StepperExpressionStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/ExpressionStatement'; +import { + StepperVariableDeclaration, + StepperVariableDeclarator +} from 'js-slang/dist/stepper/stepperV2/nodes/Statement/VariableDeclaration'; const SubstDefaultText = () => { return ( @@ -72,6 +74,7 @@ type SubstVisualizerPropsAST = { }; const SideContentSubstVisualizer: React.FC = props => { + console.log(props); const [stepValue, setStepValue] = useState(1); const lastStepValue = props.content.length; const hasRunCode = lastStepValue !== 0; @@ -82,7 +85,6 @@ const SideContentSubstVisualizer: React.FC = props => { ); // set source mode as 2 useEffect(() => { - HighlightRulesSelector(2); ModeSelector(2); }, []); @@ -119,7 +121,13 @@ const SideContentSubstVisualizer: React.FC = props => { const getExplanation = useCallback( (value: number): string => { const contIndex = value <= lastStepValue ? value - 1 : 0; - return props.content[contIndex][2]; + // Right now, prioritize the first marker + const markers = props.content[contIndex].markers; + if (markers === undefined || markers[0] === undefined) { + return '...'; + } else { + return markers[0].explanation ?? '...'; + } }, [lastStepValue, props.content] ); @@ -173,104 +181,109 @@ const SideContentSubstVisualizer: React.FC = props => { }; /////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// -// Iterative solution: get marked position for custom markers -// Normally, this will be handled using ACEEditor -function CustomASTRenderer(props: IStepperPropContents) { - const getStringWithMarker = useCallback(() => { - return toStringWithMarker(props); - }, [props]); - return ( -
- {getStringWithMarker().map(content => ( - {content['text']} - ))} -
- ); -} - -/* -function StepperDisplayer(props: IStepperPropContents) { - const getNodeType = useCallback( - (node: StepperExpression): string => { - if (props[1] === node && props[2] === 'before') { - return 'beforeMarker'; +function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { + function renderNode(node: StepperBaseNode): React.ReactNode { + const wrapMarkerStyle = (renderNode: React.ReactNode): React.ReactNode => { + if (props.markers === undefined) { + return renderNode; } - if (props[1] === node && props[2] === 'after') { - return 'afterMarker'; - } - return ''; - }, - [props] - ); - - // TODO: Move this logic from frontend to js-slang - const convertNode = useCallback( - (node: StepperExpression): React.ReactNode => { - const convertors = { - Literal(node: StepperLiteral) { - return {node.value}; - }, - UnaryExpression(node: StepperUnaryExpression) { - return ( - - {` ${node.operator}`} - {convertNode(node.argument)} - - ); - }, - BinaryExpression(node: StepperBinaryExpression) { - return ( - - {convertNode(node.left)} - {` ${node.operator} `} - {convertNode(node.right)} - - ); + var returnNode = {renderNode}; + props.markers.forEach(marker => { + if (marker.redex === node) { + returnNode = {returnNode}; } - }; - const convertor = convertors[node.type]; - // @ts-expect-error node actually has type StepperExpression - const converted = convertor(node); - if (getNodeType(node) === '') { - return {converted}; - } else { + }); + return returnNode; + }; + + const renderers = { + Literal(node: StepperLiteral) { + return {node.value}; + }, + Identifier(node: StepperIdentifier) { + return {node.name}; + }, + UnaryExpression(node: StepperUnaryExpression) { return ( - - - -
- {'Contraction rule '} - - {getNodeType(node) === 'beforeMarker' ? 'E1 -> E2' : 'finished'} - -
-
- - } - > - {converted} -
+ + {` ${node.operator}`} + {renderNode(node.argument)} + + ); + }, + BinaryExpression(node: StepperBinaryExpression) { + // TODO: check precedence + return ( + + {renderNode(node.left)} + {` ${node.operator} `} + {renderNode(node.right)} + + ); + }, + Program(node: StepperProgram) { + return ( + + {node.body.map(ast => ( +
{renderNode(ast)}
+ ))} +
+ ); + }, + BlockStatement(node: StepperBlockStatement) { + return ( + + {'{'} + {node.body.map(ast => ( +
+ {renderNode(ast)} +
+ ))} + {'}'} +
+ ); + }, + ExpressionStatement(node: StepperExpressionStatement) { + return ( + + {renderNode(node.expression)} + {';'} + + ); + }, + VariableDeclaration(node: StepperVariableDeclaration) { + return ( + + {node.kind} + {node.declarations.map((ast, idx) => ( + + {idx !== 0 && ', '} + {renderNode(ast)} + + ))} + {';'} + + ); + }, + VariableDeclarator(node: StepperVariableDeclarator) { + return ( + + {renderNode(node.id)} + {' = '} + {node.init ? renderNode(node.init) : 'undefined'} ); } - }, - [props, getNodeType] - ); - - const getConvertedNode = useCallback((): React.ReactNode => { - return convertNode(props[0]); - }, [props, convertNode]); + }; - return
{getConvertedNode()}
; + // @ts-ignore + const renderer = renderers[node.type]; + return renderer ? wrapMarkerStyle(renderer(node)) : '...'; + } + const getDisplayedNode = useCallback((): React.ReactNode => { + return renderNode(props.ast); + }, [props]); + return
{getDisplayedNode()}
; } -*/ + export default SideContentSubstVisualizer; diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index d22d6b71a0..ef505fd985 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -431,6 +431,10 @@ $code-color-notification: #f9f0d7; color: #F89210; } + .stepper-identifier { + color: yellow; + } + .stepper-display { font-family: 'Inconsolata', 'Consolas', monospace; } From 49e59c23583495f2d432013f9ed3069c85c5cb0c Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Sat, 22 Mar 2025 15:45:58 +0800 Subject: [PATCH 05/12] feat: add mu term handler --- .../content/SideContentSubstVisualizer.tsx | 334 +++++++++++++++++- src/styles/_workspace.scss | 9 + 2 files changed, 325 insertions(+), 18 deletions(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index ba77111516..000cc34b54 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -1,6 +1,16 @@ import 'js-slang/dist/editors/ace/theme/source'; -import { Button, ButtonGroup, Card, Classes, Divider, Pre, Slider } from '@blueprintjs/core'; +import { + Button, + ButtonGroup, + Card, + Classes, + Divider, + Icon, + Popover, + Pre, + Slider +} from '@blueprintjs/core'; import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; @@ -22,6 +32,13 @@ import { StepperVariableDeclaration, StepperVariableDeclarator } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/VariableDeclaration'; +import { StepperArrowFunctionExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/ArrowFunctionExpression'; +import { StepperConditionalExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/ConditionalExpression'; +import { StepperFunctionApplication } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/FunctionApplication'; +import { StepperExpression } from 'js-slang/dist/stepper/stepperV2/nodes'; +import { StepperIfStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/IfStatement'; +import { StepperReturnStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/ReturnStatement'; +import { astToString } from 'js-slang/dist/utils/ast/astToString'; const SubstDefaultText = () => { return ( @@ -181,9 +198,25 @@ const SideContentSubstVisualizer: React.FC = props => { }; /////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// +function composeStyleWrapper(first: StyleWrapper | undefined, second: StyleWrapper | undefined): StyleWrapper | undefined { + if (first === undefined && second === undefined) { + return undefined; + } else if (first === undefined) { + return second; + } else if (second === undefined) { + return first; + } + + return (node: StepperBaseNode) => (rawStyle: React.ReactNode) => { + const immediateStyle = first(node)(rawStyle); + return second(node)(immediateStyle); + } +} + +type StyleWrapper = (node: StepperBaseNode) => (rawStyle: React.ReactNode) => React.ReactNode; function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { - function renderNode(node: StepperBaseNode): React.ReactNode { - const wrapMarkerStyle = (renderNode: React.ReactNode): React.ReactNode => { + function markerStyleWrapper(node: StepperBaseNode) { + return (renderNode: React.ReactNode) => { if (props.markers === undefined) { return renderNode; } @@ -194,11 +227,67 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { } }); return returnNode; + } + } + function renderNode( + currentNode: StepperBaseNode, + parentNode?: StepperBaseNode, + isRight?: boolean, + styleWrapper?: StyleWrapper + ): React.ReactNode { + const renderArguments = (nodes: StepperExpression[]) => { + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, undefined, undefined, styleWrapper) + ); + var renderedArguments = args.slice(1).reduce( + (result, item) => ( + + {result} + {', '} + {item} + + ), + args[0] + ); + renderedArguments = ( + + {'('} + {renderedArguments} + {')'} + + ); + return renderedArguments; + }; + + const renderFunctionArguments = (nodes: StepperExpression[]) => { + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, undefined, undefined, styleWrapper) + ); + var renderedArguments = args.slice(1).reduce( + (result, item) => ( + + {result} + {', '} + {item} + + ), + args[0] + ); + if (args.length !== 1) { + renderedArguments = ( + + {'('} + {renderedArguments} + {')'} + + ); + } + return renderedArguments; }; const renderers = { Literal(node: StepperLiteral) { - return {node.value}; + return {node.value?.toString()}; }, Identifier(node: StepperIdentifier) { return {node.name}; @@ -207,17 +296,81 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { return ( {` ${node.operator}`} - {renderNode(node.argument)} + {renderNode(node.argument, node, undefined, styleWrapper)} ); }, BinaryExpression(node: StepperBinaryExpression) { - // TODO: check precedence return ( - {renderNode(node.left)} + {renderNode(node.left, node, false, styleWrapper)} {` ${node.operator} `} - {renderNode(node.right)} + {renderNode(node.right, node, true, styleWrapper)} + + ); + }, + ConditionalExpression(node: StepperConditionalExpression) { + return ( + + {renderNode(node.test, node, undefined, styleWrapper)} + {` ? `} + {renderNode(node.consequent, node, undefined, styleWrapper)} + {` : `} + {renderNode(node.alternate, node, undefined, styleWrapper)} + + ); + }, + ArrowFunctionExpression(node: StepperArrowFunctionExpression) { + function muTermStyleWrapper(targetNode: StepperBaseNode) { + if (targetNode.type === 'Identifier') { + if ((targetNode as StepperIdentifier).name === node.name) { + const reference = astToString(node); + return (styledNode: React.ReactNode) => ( + + + + {' Function definition'} +
+                          {reference}
+                        
+ + } + > + {styledNode} +
+
+ ); + } + } + return (styledNode: React.ReactNode) => styledNode; + } + return ( + + {renderFunctionArguments(node.params)} + {' => '} + {renderNode(node.body, undefined, undefined, composeStyleWrapper(styleWrapper, muTermStyleWrapper))} + + ); + }, + CallExpression(node: StepperFunctionApplication) { + var renderedCallee = renderNode(node.callee, undefined, undefined, styleWrapper); + if (node.callee.type !== 'Identifier') { + renderedCallee = ( + + {'('} + {renderedCallee} + {')'} + + ); + } + return ( + + {renderedCallee} + {renderArguments(node.arguments)} ); }, @@ -225,18 +378,30 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { return ( {node.body.map(ast => ( -
{renderNode(ast)}
+
{renderNode(ast, node, undefined, styleWrapper)}
))}
); }, + IfStatement(node: StepperIfStatement) { + return {'TO BE IMPLEMENTED'}; + }, + ReturnStatement(node: StepperReturnStatement) { + return ( + + {'return '} + {node.argument && renderNode(node.argument, undefined, undefined, styleWrapper)} + {';'} + + ); + }, BlockStatement(node: StepperBlockStatement) { return ( {'{'} {node.body.map(ast => ( -
- {renderNode(ast)} +
+ {renderNode(ast, undefined, undefined, styleWrapper)}
))} {'}'} @@ -246,7 +411,7 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { ExpressionStatement(node: StepperExpressionStatement) { return ( - {renderNode(node.expression)} + {renderNode(node.expression, undefined, undefined, styleWrapper)} {';'} ); @@ -258,7 +423,7 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { {node.declarations.map((ast, idx) => ( {idx !== 0 && ', '} - {renderNode(ast)} + {renderNode(ast, undefined, undefined, styleWrapper)} ))} {';'} @@ -268,22 +433,155 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { VariableDeclarator(node: StepperVariableDeclarator) { return ( - {renderNode(node.id)} + {renderNode(node.id, undefined, undefined, styleWrapper)} {' = '} - {node.init ? renderNode(node.init) : 'undefined'} + {node.init ? renderNode(node.init, undefined, undefined, styleWrapper) : 'undefined'} ); } }; // @ts-ignore - const renderer = renderers[node.type]; - return renderer ? wrapMarkerStyle(renderer(node)) : '...'; + const renderer = renderers[currentNode.type]; + const isParenthesis = expressionNeedsParenthesis(currentNode, parentNode, isRight); + var result = renderer ? renderer(currentNode) : `<${currentNode.type}>`; + if (isParenthesis) { + result = ( + + {'('} + {result} + {')'} + + ); + } + // custom wrapper style + if (styleWrapper) { + result = styleWrapper(currentNode)(result); + } + return result; } const getDisplayedNode = useCallback((): React.ReactNode => { - return renderNode(props.ast); + return renderNode(props.ast, undefined, undefined, markerStyleWrapper); }, [props]); return
{getDisplayedNode()}
; } +/////////// Parenthesis handling //////////// +const OPERATOR_PRECEDENCE = { + '||': 2, + '??': 3, + '&&': 4, + '|': 5, + '^': 6, + '&': 7, + '==': 8, + '!=': 8, + '===': 8, + '!==': 8, + '<': 9, + '>': 9, + '<=': 9, + '>=': 9, + in: 9, + instanceof: 9, + '<<': 10, + '>>': 10, + '>>>': 10, + '+': 11, + '-': 11, + '*': 12, + '%': 12, + '/': 12, + '**': 13 +}; +const NEEDS_PARENTHESES = 17; +const EXPRESSIONS_PRECEDENCE = { + // Definitions + ArrayExpression: 20, + TaggedTemplateExpression: 20, + ThisExpression: 20, + Identifier: 20, + PrivateIdentifier: 20, + Literal: 18, + TemplateLiteral: 20, + Super: 20, + SequenceExpression: 20, + // Operations + MemberExpression: 19, + ChainExpression: 19, + CallExpression: 19, + NewExpression: 19, + // Other definitions + ArrowFunctionExpression: NEEDS_PARENTHESES, + ClassExpression: NEEDS_PARENTHESES, + FunctionExpression: NEEDS_PARENTHESES, + ObjectExpression: NEEDS_PARENTHESES, + // Other operations + UpdateExpression: 16, + UnaryExpression: 15, + AwaitExpression: 15, + BinaryExpression: 14, + LogicalExpression: 13, + ConditionalExpression: 4, + AssignmentExpression: 3, + YieldExpression: 2, + RestElement: 1 +}; + +// Inspired by astring +function expressionNeedsParenthesis( + node: StepperBaseNode, + parentNode?: StepperBaseNode, + isRightHand?: boolean +) { + if (parentNode === undefined) { + return false; + } + + const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type as keyof typeof EXPRESSIONS_PRECEDENCE]; + if (nodePrecedence === NEEDS_PARENTHESES) { + return true; + } + const parentNodePrecedence = + EXPRESSIONS_PRECEDENCE[parentNode.type as keyof typeof EXPRESSIONS_PRECEDENCE]; + if (nodePrecedence === undefined || parentNodePrecedence === undefined) { + return false; + } + + if (nodePrecedence !== parentNodePrecedence) { + return ( + (!isRightHand && nodePrecedence === 15 && parentNodePrecedence === 14) || + nodePrecedence < parentNodePrecedence + ); + } + + if (!('operator' in node) || !('operator' in parentNode)) { + return false; + } + + if (nodePrecedence !== 13 && nodePrecedence !== 14) { + // Not a `LogicalExpression` or `BinaryExpression` + return false; + } + if (node.operator === '**' && parentNode.operator === '**') { + // Exponentiation operator has right-to-left associativity + return !isRightHand; + } + if ( + nodePrecedence === 13 && + parentNodePrecedence === 13 && + (node.operator === '??' || parentNode.operator === '??') + ) { + return true; + } + + const nodeOperatorPrecedence = + OPERATOR_PRECEDENCE[node.operator as keyof typeof OPERATOR_PRECEDENCE]; + const parentNodeOperatorPrecedence = + OPERATOR_PRECEDENCE[parentNode.operator as keyof typeof OPERATOR_PRECEDENCE]; + return isRightHand + ? nodeOperatorPrecedence <= parentNodeOperatorPrecedence + : nodeOperatorPrecedence <= parentNodeOperatorPrecedence; +} + export default SideContentSubstVisualizer; diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index ef505fd985..8116e3deb6 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -435,6 +435,15 @@ $code-color-notification: #f9f0d7; color: yellow; } + .stepper-mu-term { + background: rgba(255, 83, 83, 0.35); + pointer-events: auto; + cursor: pointer; + z-index: 20; + padding: 0px 3px; + border-radius: 5px; + } + .stepper-display { font-family: 'Inconsolata', 'Consolas', monospace; } From 42fd50f03dbda9df2e1b5a8781a7949d248d119a Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Wed, 9 Apr 2025 02:15:10 +0800 Subject: [PATCH 06/12] chore: refactoring --- .../content/SideContentSubstVisualizer.tsx | 595 +++++++++++------- src/styles/_workspace.scss | 2 + 2 files changed, 366 insertions(+), 231 deletions(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 000cc34b54..8d1836deb4 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -19,26 +19,29 @@ import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; -import { IStepperPropContents } from 'js-slang/dist/stepper/stepperV2'; -import { StepperBaseNode } from 'js-slang/dist/stepper/stepperV2/interface'; -import { StepperLiteral } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/Literal'; -import { StepperUnaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/UnaryExpression'; -import { StepperBinaryExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/BinaryExpression'; -import { StepperProgram } from 'js-slang/dist/stepper/stepperV2/nodes/Program'; -import { StepperBlockStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/BlockStatement'; -import { StepperIdentifier } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/Identifier'; -import { StepperExpressionStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/ExpressionStatement'; +import { IStepperPropContents } from 'js-slang/dist/tracer'; +import { StepperBaseNode } from 'js-slang/dist/tracer/interface'; +import { StepperLiteral } from 'js-slang/dist/tracer/nodes/Expression/Literal'; +import { StepperUnaryExpression } from 'js-slang/dist/tracer/nodes/Expression/UnaryExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/tracer/nodes/Expression/BinaryExpression'; +import { StepperProgram } from 'js-slang/dist/tracer/nodes/Program'; +import { StepperBlockStatement } from 'js-slang/dist/tracer/nodes/Statement/BlockStatement'; +import { StepperIdentifier } from 'js-slang/dist/tracer/nodes/Expression/Identifier'; +import { StepperExpressionStatement } from 'js-slang/dist/tracer/nodes/Statement/ExpressionStatement'; import { StepperVariableDeclaration, StepperVariableDeclarator -} from 'js-slang/dist/stepper/stepperV2/nodes/Statement/VariableDeclaration'; -import { StepperArrowFunctionExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/ArrowFunctionExpression'; -import { StepperConditionalExpression } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/ConditionalExpression'; -import { StepperFunctionApplication } from 'js-slang/dist/stepper/stepperV2/nodes/Expression/FunctionApplication'; -import { StepperExpression } from 'js-slang/dist/stepper/stepperV2/nodes'; -import { StepperIfStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/IfStatement'; -import { StepperReturnStatement } from 'js-slang/dist/stepper/stepperV2/nodes/Statement/ReturnStatement'; +} from 'js-slang/dist/tracer/nodes/Statement/VariableDeclaration'; +import { StepperArrowFunctionExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrowFunctionExpression'; +import { StepperConditionalExpression } from 'js-slang/dist/tracer/nodes/Expression/ConditionalExpression'; +import { StepperFunctionApplication } from 'js-slang/dist/tracer/nodes/Expression/FunctionApplication'; +import { StepperExpression } from 'js-slang/dist/tracer/nodes'; +import { StepperIfStatement } from 'js-slang/dist/tracer/nodes/Statement/IfStatement'; +import { StepperReturnStatement } from 'js-slang/dist/tracer/nodes/Statement/ReturnStatement'; import { astToString } from 'js-slang/dist/utils/ast/astToString'; +import { StepperBlockExpression } from 'js-slang/dist/tracer/nodes/Expression/BlockExpression'; +import { StepperFunctionDeclaration } from 'js-slang/dist/tracer/nodes/Statement/FunctionDeclaration'; +import { StepperArrayExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrayExpression'; const SubstDefaultText = () => { return ( @@ -189,80 +192,108 @@ const SideContentSubstVisualizer: React.FC = props => {
{hasRunCode ? : } {hasRunCode ? : null} -
-
Expression stepper
-
{'Double arrows << and >> are replaced with stepFirst and stepLast.'}
-
); }; -/////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// -function composeStyleWrapper(first: StyleWrapper | undefined, second: StyleWrapper | undefined): StyleWrapper | undefined { - if (first === undefined && second === undefined) { - return undefined; - } else if (first === undefined) { - return second; - } else if (second === undefined) { - return first; - } - return (node: StepperBaseNode) => (rawStyle: React.ReactNode) => { - const immediateStyle = first(node)(rawStyle); - return second(node)(immediateStyle); - } +/* + Custom AST renderer for Stepper (Inspired by astring library) + This custom AST renderer utilizing the recursive approach of handling rendering of various StepperNodes by + using nested
and . Unlike React-ace, using our own renderer make our stepper more customizable. For example, + we can add a code component that is hoverable by using with blueprint tooltip. +*/ + +/** RenderContext holds relevant information to handle rendering. This will be carried along the recursive renderNode function + * @params parentNode and isRight are used to dictate whether this node requires parenthesis or not + * @params styleWrapper composes the necessary styles being passed. + */ +interface RenderContext { + parentNode?: StepperBaseNode + isRight?: boolean // specified for binary expression + styleWrapper: StyleWrapper } -type StyleWrapper = (node: StepperBaseNode) => (rawStyle: React.ReactNode) => React.ReactNode; -function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { - function markerStyleWrapper(node: StepperBaseNode) { - return (renderNode: React.ReactNode) => { - if (props.markers === undefined) { - return renderNode; - } - var returnNode = {renderNode}; - props.markers.forEach(marker => { - if (marker.redex === node) { - returnNode = {returnNode}; - } - }); - return returnNode; - } - } - function renderNode( - currentNode: StepperBaseNode, - parentNode?: StepperBaseNode, - isRight?: boolean, - styleWrapper?: StyleWrapper - ): React.ReactNode { - const renderArguments = (nodes: StepperExpression[]) => { - const args: React.ReactNode[] = nodes.map(arg => - renderNode(arg, undefined, undefined, styleWrapper) - ); - var renderedArguments = args.slice(1).reduce( - (result, item) => ( - - {result} - {', '} - {item} - - ), - args[0] +/* + StyleWrapper is a function that returns a styling function based on the node. For example, + const wrapLiteral: StyleWrapper = (node) + => (preformatted) => node.type === "Literal" ?
preformatted
: preformatted; + makes the default result from renderNode(node) wrapped with className stepper-literal for literal AST. +*/ +type StyleWrapper = (node: StepperBaseNode) => (preformatted: React.ReactNode) => React.ReactNode; + +// composeStyleWrapper takes two style wrappers and merge its effect together. +function composeStyleWrapper( + first: StyleWrapper | undefined, + second: StyleWrapper | undefined +): StyleWrapper | undefined { + return first === undefined && second === undefined + ? undefined + : first === undefined + ? second + : second === undefined + ? first + : (node: StepperBaseNode) => (preformatted: React.ReactNode) => { + const afterFirstStyle = first(node)(preformatted); + return second(node)(afterFirstStyle); + }; +} + +/** + * renderNode renders Stepper AST to React ReactNode + * @param currentNode + * @param renderContext + */ +function renderNode( + currentNode: StepperBaseNode, + renderContext: RenderContext +): React.ReactNode { + const styleWrapper = renderContext.styleWrapper; + + const renderers = { + Literal(node: StepperLiteral) { + return + {node.raw ? node.raw : JSON.stringify(node.value)} + ; + }, + Identifier(node: StepperIdentifier) { + return {node.name}; + }, + // Expressions + UnaryExpression(node: StepperUnaryExpression) { + return ( + + {`${node.operator}`} + {renderNode(node.argument, {parentNode: node, styleWrapper: styleWrapper})} + ); - renderedArguments = ( + }, + BinaryExpression(node: StepperBinaryExpression) { + return ( - {'('} - {renderedArguments} - {')'} + {renderNode(node.left, {parentNode: node, isRight: false, styleWrapper: styleWrapper})} + {` ${node.operator} `} + {renderNode(node.right, {parentNode: node, isRight: true, styleWrapper: styleWrapper})} ); - return renderedArguments; - }; - - const renderFunctionArguments = (nodes: StepperExpression[]) => { - const args: React.ReactNode[] = nodes.map(arg => - renderNode(arg, undefined, undefined, styleWrapper) + }, + ConditionalExpression(node: StepperConditionalExpression) { + return ( + + {renderNode(node.test, { styleWrapper: styleWrapper})} + {` ? `} + {renderNode(node.consequent, { styleWrapper: styleWrapper})} + {` : `} + {renderNode(node.alternate, { styleWrapper: styleWrapper})} + ); + }, + ArrayExpression(node: StepperArrayExpression) { + // Render all arguments inside an array + const args: React.ReactNode[] = node.elements + .filter(arg => arg !== null) + .map(arg => renderNode(arg, { styleWrapper: styleWrapper})); + var renderedArguments = args.slice(1).reduce( (result, item) => ( @@ -273,91 +304,83 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { ), args[0] ); - if (args.length !== 1) { - renderedArguments = ( - - {'('} - {renderedArguments} - {')'} - - ); - } - return renderedArguments; - }; - - const renderers = { - Literal(node: StepperLiteral) { - return {node.value?.toString()}; - }, - Identifier(node: StepperIdentifier) { - return {node.name}; - }, - UnaryExpression(node: StepperUnaryExpression) { - return ( - - {` ${node.operator}`} - {renderNode(node.argument, node, undefined, styleWrapper)} - - ); - }, - BinaryExpression(node: StepperBinaryExpression) { - return ( - - {renderNode(node.left, node, false, styleWrapper)} - {` ${node.operator} `} - {renderNode(node.right, node, true, styleWrapper)} - - ); - }, - ConditionalExpression(node: StepperConditionalExpression) { - return ( - - {renderNode(node.test, node, undefined, styleWrapper)} - {` ? `} - {renderNode(node.consequent, node, undefined, styleWrapper)} - {` : `} - {renderNode(node.alternate, node, undefined, styleWrapper)} - - ); - }, - ArrowFunctionExpression(node: StepperArrowFunctionExpression) { + return ( + + {'['} + {renderedArguments} + {']'} + + ); + }, + ArrowFunctionExpression(node: StepperArrowFunctionExpression) { + /** + * Add hovering effect to children nodes only if it is an identifier with the name + * corresponding to the name of lambda expression + */ function muTermStyleWrapper(targetNode: StepperBaseNode) { - if (targetNode.type === 'Identifier') { - if ((targetNode as StepperIdentifier).name === node.name) { - const reference = astToString(node); - return (styledNode: React.ReactNode) => ( - - - - {' Function definition'} -
-                          {reference}
-                        
-
- } - > - {styledNode} - -
- ); - } + if (targetNode.type === 'Identifier' + && (targetNode as StepperIdentifier).name === node.name) { + function addHovering(preprocessed: React.ReactNode): React.ReactNode { + const functionDefinition = astToString(node); + return + + + {' Function definition'} +
+                                  {functionDefinition}
+                                
+ + } + > + {preprocessed} +
+
+ } + return addHovering; + } else { + // Do nothing + return (preprocessed: React.ReactNode) => preprocessed; } - return (styledNode: React.ReactNode) => styledNode; } - return ( + + // If the name is specified, render the name and add hovering for the body. + return node.name + ? + + + {' Function definition'} +
+                  {astToString(node)}
+                
+ + } + > + {node.name} +
+
+ : ( {renderFunctionArguments(node.params)} {' => '} - {renderNode(node.body, undefined, undefined, composeStyleWrapper(styleWrapper, muTermStyleWrapper))} + {renderNode( + node.body, + { + styleWrapper: composeStyleWrapper(styleWrapper, muTermStyleWrapper)! + } + )} ); }, CallExpression(node: StepperFunctionApplication) { - var renderedCallee = renderNode(node.callee, undefined, undefined, styleWrapper); + var renderedCallee = renderNode(node.callee, { styleWrapper: styleWrapper}); if (node.callee.type !== 'Identifier') { renderedCallee = ( @@ -378,19 +401,34 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { return ( {node.body.map(ast => ( -
{renderNode(ast, node, undefined, styleWrapper)}
+
{renderNode(ast, { styleWrapper: styleWrapper})}
))}
); }, IfStatement(node: StepperIfStatement) { - return {'TO BE IMPLEMENTED'}; + return + + {"if "} + {"("} + {renderNode(node.test, { styleWrapper: styleWrapper})} + {") "} + + {renderNode(node.consequent, { styleWrapper: styleWrapper})} + { + node.alternate && + + {" else "} + {renderNode(node.alternate!, { styleWrapper: styleWrapper})} + + } + }, ReturnStatement(node: StepperReturnStatement) { return ( {'return '} - {node.argument && renderNode(node.argument, undefined, undefined, styleWrapper)} + {node.argument && renderNode(node.argument, { styleWrapper: styleWrapper})} {';'} ); @@ -399,23 +437,47 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { return ( {'{'} +
{node.body.map(ast => (
- {renderNode(ast, undefined, undefined, styleWrapper)} + {renderNode(ast, { styleWrapper: styleWrapper})}
))} +
{'}'}
); }, + BlockExpression(node: StepperBlockExpression) { + return ( + + {'{'} + {node.body.map(ast => ( +
+ {renderNode(ast, { styleWrapper: styleWrapper})} +
+ ))} + {'};'} +
+ ); + }, ExpressionStatement(node: StepperExpressionStatement) { return ( - {renderNode(node.expression, undefined, undefined, styleWrapper)} + {renderNode(node.expression, { styleWrapper: styleWrapper})} {';'} ); }, + FunctionDeclaration(node: StepperFunctionDeclaration) { + return ( + + {`function ${node.id.name}`} + {renderArguments(node.params)} + {" "}{renderNode(node.body, { styleWrapper: styleWrapper})} + + ); + }, VariableDeclaration(node: StepperVariableDeclaration) { return ( @@ -423,7 +485,7 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { {node.declarations.map((ast, idx) => ( {idx !== 0 && ', '} - {renderNode(ast, undefined, undefined, styleWrapper)} + {renderNode(ast, { styleWrapper: styleWrapper})} ))} {';'} @@ -433,40 +495,166 @@ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { VariableDeclarator(node: StepperVariableDeclarator) { return ( - {renderNode(node.id, undefined, undefined, styleWrapper)} + {renderNode(node.id, { styleWrapper: styleWrapper})} {' = '} - {node.init ? renderNode(node.init, undefined, undefined, styleWrapper) : 'undefined'} + {node.init ? renderNode(node.init, { styleWrapper: styleWrapper}) : 'undefined'} ); } - }; + } - // @ts-ignore - const renderer = renderers[currentNode.type]; - const isParenthesis = expressionNeedsParenthesis(currentNode, parentNode, isRight); - var result = renderer ? renderer(currentNode) : `<${currentNode.type}>`; - if (isParenthesis) { - result = ( + // Additional renderers + const renderFunctionArguments = (nodes: StepperExpression[]) => { + const args: React.ReactNode[] = nodes.map(arg =>renderNode(arg, { styleWrapper: styleWrapper})); + var renderedArguments = args.slice(1).reduce( + (result, item) => ( - {'('} {result} + {', '} + {item} + + ), + args[0] + ); + if (args.length !== 1) { + renderedArguments = ( + + {'('} + {renderedArguments} {')'} ); } - // custom wrapper style - if (styleWrapper) { - result = styleWrapper(currentNode)(result); - } - return result; + return renderedArguments; + }; + + const renderArguments = (nodes: StepperExpression[]) => { + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, {styleWrapper: styleWrapper}) + ); + var renderedArguments = args.slice(1).reduce( + (result, item) => ( + + {result} + {', '} + {item} + + ), + args[0] + ); + renderedArguments = ( + + {'('} + {renderedArguments} + {')'} + + ); + return renderedArguments; + }; + + // Entry point of rendering + const renderer = renderers[currentNode.type as keyof typeof renderers]; + const isParenthesis = expressionNeedsParenthesis(currentNode, renderContext.parentNode, renderContext.isRight); + var result: React.ReactNode = renderer + // @ts-expect-error All subclasses of stepper base node has its corresponding renderes + ? renderer(currentNode) + : `<${currentNode.type}>`; // For debugging in case some AST renderer has not been implemented yet + if (isParenthesis) { + result = {'('}{result}{')'}; + } + // custom wrapper style + if (styleWrapper) { + result = styleWrapper(currentNode)(result); } + return result; +} +/////////////////////////////////// Custom AST Renderer for Stepper ////////////////////////////////// + +/** + * A React component that handles rendering + */ +function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { + function markerStyleWrapper(node: StepperBaseNode) { + return (renderNode: React.ReactNode) => { + if (props.markers === undefined) { + return renderNode; + } + var returnNode = {renderNode}; + props.markers.forEach(marker => { + if (marker.redex === node) { + returnNode = {returnNode}; + } + }); + return returnNode; + }; + } + const getDisplayedNode = useCallback((): React.ReactNode => { - return renderNode(props.ast, undefined, undefined, markerStyleWrapper); + return renderNode(props.ast, { + styleWrapper: markerStyleWrapper + }); }, [props]); return
{getDisplayedNode()}
; } -/////////// Parenthesis handling //////////// +/** + * expressionNeedsParenthesis + * checking whether the there should be parentheses wrapped around the node or not + */ +function expressionNeedsParenthesis( + node: StepperBaseNode, + parentNode?: StepperBaseNode, + isRightHand?: boolean +) { + if (parentNode === undefined) { + return false; + } + + const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type as keyof typeof EXPRESSIONS_PRECEDENCE]; + if (nodePrecedence === NEEDS_PARENTHESES) { + return true; + } + const parentNodePrecedence = + EXPRESSIONS_PRECEDENCE[parentNode.type as keyof typeof EXPRESSIONS_PRECEDENCE]; + if (nodePrecedence === undefined || parentNodePrecedence === undefined) { + return false; + } + + if (nodePrecedence !== parentNodePrecedence) { + return ( + (!isRightHand && nodePrecedence === 15 && parentNodePrecedence === 14) || + nodePrecedence < parentNodePrecedence + ); + } + + if (!('operator' in node) || !('operator' in parentNode)) { + return false; + } + + if (nodePrecedence !== 13 && nodePrecedence !== 14) { + // Not a `LogicalExpression` or `BinaryExpression` + return false; + } + if (node.operator === '**' && parentNode.operator === '**') { + // Exponentiation operator has right-to-left associativity + return !isRightHand; + } + if ( + nodePrecedence === 13 && + parentNodePrecedence === 13 && + (node.operator === '??' || parentNode.operator === '??') + ) { + return true; + } + + const nodeOperatorPrecedence = + OPERATOR_PRECEDENCE[node.operator as keyof typeof OPERATOR_PRECEDENCE]; + const parentNodeOperatorPrecedence = + OPERATOR_PRECEDENCE[parentNode.operator as keyof typeof OPERATOR_PRECEDENCE]; + return isRightHand + ? nodeOperatorPrecedence <= parentNodeOperatorPrecedence + : nodeOperatorPrecedence <= parentNodeOperatorPrecedence; +} const OPERATOR_PRECEDENCE = { '||': 2, '??': 3, @@ -528,60 +716,5 @@ const EXPRESSIONS_PRECEDENCE = { RestElement: 1 }; -// Inspired by astring -function expressionNeedsParenthesis( - node: StepperBaseNode, - parentNode?: StepperBaseNode, - isRightHand?: boolean -) { - if (parentNode === undefined) { - return false; - } - - const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type as keyof typeof EXPRESSIONS_PRECEDENCE]; - if (nodePrecedence === NEEDS_PARENTHESES) { - return true; - } - const parentNodePrecedence = - EXPRESSIONS_PRECEDENCE[parentNode.type as keyof typeof EXPRESSIONS_PRECEDENCE]; - if (nodePrecedence === undefined || parentNodePrecedence === undefined) { - return false; - } - - if (nodePrecedence !== parentNodePrecedence) { - return ( - (!isRightHand && nodePrecedence === 15 && parentNodePrecedence === 14) || - nodePrecedence < parentNodePrecedence - ); - } - - if (!('operator' in node) || !('operator' in parentNode)) { - return false; - } - - if (nodePrecedence !== 13 && nodePrecedence !== 14) { - // Not a `LogicalExpression` or `BinaryExpression` - return false; - } - if (node.operator === '**' && parentNode.operator === '**') { - // Exponentiation operator has right-to-left associativity - return !isRightHand; - } - if ( - nodePrecedence === 13 && - parentNodePrecedence === 13 && - (node.operator === '??' || parentNode.operator === '??') - ) { - return true; - } - - const nodeOperatorPrecedence = - OPERATOR_PRECEDENCE[node.operator as keyof typeof OPERATOR_PRECEDENCE]; - const parentNodeOperatorPrecedence = - OPERATOR_PRECEDENCE[parentNode.operator as keyof typeof OPERATOR_PRECEDENCE]; - return isRightHand - ? nodeOperatorPrecedence <= parentNodeOperatorPrecedence - : nodeOperatorPrecedence <= parentNodeOperatorPrecedence; -} export default SideContentSubstVisualizer; diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 8116e3deb6..6f80530a9d 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -449,6 +449,7 @@ $code-color-notification: #f9f0d7; } .beforeMarker { + display: inline-block; background: rgba(179, 101, 57, 0.75); pointer-events: auto; cursor: pointer; @@ -460,6 +461,7 @@ $code-color-notification: #f9f0d7; } .afterMarker { + display: inline-block; background: green; pointer-events: auto; cursor: pointer; From 76b16ff1744fb1f90df63de46e2f52d362c89a85 Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Wed, 9 Apr 2025 02:43:14 +0800 Subject: [PATCH 07/12] chore: lint fix --- .../content/SideContentSubstVisualizer.tsx | 502 +++++++++--------- src/pages/playground/PlaygroundTabs.tsx | 3 +- src/styles/_workspace.scss | 10 +- 3 files changed, 261 insertions(+), 254 deletions(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 8d1836deb4..423b584c26 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -14,34 +14,34 @@ import { import { getHotkeyHandler, HotkeyItem } from '@mantine/hooks'; import classNames from 'classnames'; import { HighlightRulesSelector, ModeSelector } from 'js-slang/dist/editors/ace/modes/source'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; - -import { beginAlertSideContent } from '../SideContentActions'; -import { SideContentLocation, SideContentType } from '../SideContentTypes'; import { IStepperPropContents } from 'js-slang/dist/tracer'; import { StepperBaseNode } from 'js-slang/dist/tracer/interface'; +import { StepperExpression } from 'js-slang/dist/tracer/nodes'; +import { StepperArrayExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrayExpression'; +import { StepperArrowFunctionExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrowFunctionExpression'; +import { StepperBinaryExpression } from 'js-slang/dist/tracer/nodes/Expression/BinaryExpression'; +import { StepperBlockExpression } from 'js-slang/dist/tracer/nodes/Expression/BlockExpression'; +import { StepperConditionalExpression } from 'js-slang/dist/tracer/nodes/Expression/ConditionalExpression'; +import { StepperFunctionApplication } from 'js-slang/dist/tracer/nodes/Expression/FunctionApplication'; +import { StepperIdentifier } from 'js-slang/dist/tracer/nodes/Expression/Identifier'; import { StepperLiteral } from 'js-slang/dist/tracer/nodes/Expression/Literal'; import { StepperUnaryExpression } from 'js-slang/dist/tracer/nodes/Expression/UnaryExpression'; -import { StepperBinaryExpression } from 'js-slang/dist/tracer/nodes/Expression/BinaryExpression'; import { StepperProgram } from 'js-slang/dist/tracer/nodes/Program'; import { StepperBlockStatement } from 'js-slang/dist/tracer/nodes/Statement/BlockStatement'; -import { StepperIdentifier } from 'js-slang/dist/tracer/nodes/Expression/Identifier'; import { StepperExpressionStatement } from 'js-slang/dist/tracer/nodes/Statement/ExpressionStatement'; +import { StepperFunctionDeclaration } from 'js-slang/dist/tracer/nodes/Statement/FunctionDeclaration'; +import { StepperIfStatement } from 'js-slang/dist/tracer/nodes/Statement/IfStatement'; +import { StepperReturnStatement } from 'js-slang/dist/tracer/nodes/Statement/ReturnStatement'; import { StepperVariableDeclaration, StepperVariableDeclarator } from 'js-slang/dist/tracer/nodes/Statement/VariableDeclaration'; -import { StepperArrowFunctionExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrowFunctionExpression'; -import { StepperConditionalExpression } from 'js-slang/dist/tracer/nodes/Expression/ConditionalExpression'; -import { StepperFunctionApplication } from 'js-slang/dist/tracer/nodes/Expression/FunctionApplication'; -import { StepperExpression } from 'js-slang/dist/tracer/nodes'; -import { StepperIfStatement } from 'js-slang/dist/tracer/nodes/Statement/IfStatement'; -import { StepperReturnStatement } from 'js-slang/dist/tracer/nodes/Statement/ReturnStatement'; import { astToString } from 'js-slang/dist/utils/ast/astToString'; -import { StepperBlockExpression } from 'js-slang/dist/tracer/nodes/Expression/BlockExpression'; -import { StepperFunctionDeclaration } from 'js-slang/dist/tracer/nodes/Statement/FunctionDeclaration'; -import { StepperArrayExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrayExpression'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { beginAlertSideContent } from '../SideContentActions'; +import { SideContentLocation, SideContentType } from '../SideContentTypes'; const SubstDefaultText = () => { return ( @@ -196,7 +196,6 @@ const SideContentSubstVisualizer: React.FC = props => { ); }; - /* Custom AST renderer for Stepper (Inspired by astring library) This custom AST renderer utilizing the recursive approach of handling rendering of various StepperNodes by @@ -206,12 +205,12 @@ const SideContentSubstVisualizer: React.FC = props => { /** RenderContext holds relevant information to handle rendering. This will be carried along the recursive renderNode function * @params parentNode and isRight are used to dictate whether this node requires parenthesis or not - * @params styleWrapper composes the necessary styles being passed. + * @params styleWrapper composes the necessary styles being passed. */ interface RenderContext { - parentNode?: StepperBaseNode - isRight?: boolean // specified for binary expression - styleWrapper: StyleWrapper + parentNode?: StepperBaseNode; + isRight?: boolean; // specified for binary expression + styleWrapper: StyleWrapper; } /* @@ -227,34 +226,31 @@ function composeStyleWrapper( first: StyleWrapper | undefined, second: StyleWrapper | undefined ): StyleWrapper | undefined { - return first === undefined && second === undefined + return first === undefined && second === undefined ? undefined - : first === undefined - ? second - : second === undefined - ? first - : (node: StepperBaseNode) => (preformatted: React.ReactNode) => { - const afterFirstStyle = first(node)(preformatted); - return second(node)(afterFirstStyle); - }; + : first === undefined + ? second + : second === undefined + ? first + : (node: StepperBaseNode) => (preformatted: React.ReactNode) => { + const afterFirstStyle = first(node)(preformatted); + return second(node)(afterFirstStyle); + }; } /** * renderNode renders Stepper AST to React ReactNode - * @param currentNode - * @param renderContext + * @param currentNode + * @param renderContext */ -function renderNode( - currentNode: StepperBaseNode, - renderContext: RenderContext -): React.ReactNode { +function renderNode(currentNode: StepperBaseNode, renderContext: RenderContext): React.ReactNode { const styleWrapper = renderContext.styleWrapper; - + const renderers = { Literal(node: StepperLiteral) { - return - {node.raw ? node.raw : JSON.stringify(node.value)} - ; + return ( + {node.raw ? node.raw : JSON.stringify(node.value)} + ); }, Identifier(node: StepperIdentifier) { return {node.name}; @@ -264,27 +260,27 @@ function renderNode( return ( {`${node.operator}`} - {renderNode(node.argument, {parentNode: node, styleWrapper: styleWrapper})} + {renderNode(node.argument, { parentNode: node, styleWrapper: styleWrapper })} ); }, BinaryExpression(node: StepperBinaryExpression) { return ( - {renderNode(node.left, {parentNode: node, isRight: false, styleWrapper: styleWrapper})} + {renderNode(node.left, { parentNode: node, isRight: false, styleWrapper: styleWrapper })} {` ${node.operator} `} - {renderNode(node.right, {parentNode: node, isRight: true, styleWrapper: styleWrapper})} + {renderNode(node.right, { parentNode: node, isRight: true, styleWrapper: styleWrapper })} ); }, ConditionalExpression(node: StepperConditionalExpression) { return ( - {renderNode(node.test, { styleWrapper: styleWrapper})} + {renderNode(node.test, { styleWrapper: styleWrapper })} {` ? `} - {renderNode(node.consequent, { styleWrapper: styleWrapper})} + {renderNode(node.consequent, { styleWrapper: styleWrapper })} {` : `} - {renderNode(node.alternate, { styleWrapper: styleWrapper})} + {renderNode(node.alternate, { styleWrapper: styleWrapper })} ); }, @@ -292,9 +288,9 @@ function renderNode( // Render all arguments inside an array const args: React.ReactNode[] = node.elements .filter(arg => arg !== null) - .map(arg => renderNode(arg, { styleWrapper: styleWrapper})); - - var renderedArguments = args.slice(1).reduce( + .map(arg => renderNode(arg, { styleWrapper: styleWrapper })); + + const renderedArguments = args.slice(1).reduce( (result, item) => ( {result} @@ -317,39 +313,43 @@ function renderNode( * Add hovering effect to children nodes only if it is an identifier with the name * corresponding to the name of lambda expression */ - function muTermStyleWrapper(targetNode: StepperBaseNode) { - if (targetNode.type === 'Identifier' - && (targetNode as StepperIdentifier).name === node.name) { - function addHovering(preprocessed: React.ReactNode): React.ReactNode { - const functionDefinition = astToString(node); - return - - - {' Function definition'} -
-                                  {functionDefinition}
-                                
- - } - > - {preprocessed} -
-
- } - return addHovering; - } else { - // Do nothing - return (preprocessed: React.ReactNode) => preprocessed; + function muTermStyleWrapper(targetNode: StepperBaseNode) { + if ( + targetNode.type === 'Identifier' && + (targetNode as StepperIdentifier).name === node.name + ) { + function addHovering(preprocessed: React.ReactNode): React.ReactNode { + const functionDefinition = astToString(node); + return ( + + + + {' Function definition'} +
+                        {functionDefinition}
+                      
+ + } + > + {preprocessed} +
+
+ ); } + return addHovering; + } else { + // Do nothing + return (preprocessed: React.ReactNode) => preprocessed; } - - // If the name is specified, render the name and add hovering for the body. - return node.name - ? + } + + // If the name is specified, render the name and add hovering for the body. + return node.name ? ( + - : ( - - {renderFunctionArguments(node.params)} - {' => '} - {renderNode( - node.body, - { - styleWrapper: composeStyleWrapper(styleWrapper, muTermStyleWrapper)! - } - )} - - ); - }, - CallExpression(node: StepperFunctionApplication) { - var renderedCallee = renderNode(node.callee, { styleWrapper: styleWrapper}); - if (node.callee.type !== 'Identifier') { - renderedCallee = ( - - {'('} - {renderedCallee} - {')'} - - ); - } - return ( + ) : ( + + {renderFunctionArguments(node.params)} + {' => '} + {renderNode(node.body, { + styleWrapper: composeStyleWrapper(styleWrapper, muTermStyleWrapper)! + })} + + ); + }, + CallExpression(node: StepperFunctionApplication) { + let renderedCallee = renderNode(node.callee, { styleWrapper: styleWrapper }); + if (node.callee.type !== 'Identifier') { + renderedCallee = ( + {'('} {renderedCallee} - {renderArguments(node.arguments)} - - ); - }, - Program(node: StepperProgram) { - return ( - - {node.body.map(ast => ( -
{renderNode(ast, { styleWrapper: styleWrapper})}
- ))} + {')'}
); - }, - IfStatement(node: StepperIfStatement) { - return - - {"if "} - {"("} - {renderNode(node.test, { styleWrapper: styleWrapper})} - {") "} - - {renderNode(node.consequent, { styleWrapper: styleWrapper})} - { - node.alternate && - - {" else "} - {renderNode(node.alternate!, { styleWrapper: styleWrapper})} - - } - - }, - ReturnStatement(node: StepperReturnStatement) { - return ( + } + return ( + + {renderedCallee} + {renderArguments(node.arguments)} + + ); + }, + Program(node: StepperProgram) { + return ( + + {node.body.map(ast => ( +
{renderNode(ast, { styleWrapper: styleWrapper })}
+ ))} +
+ ); + }, + IfStatement(node: StepperIfStatement) { + return ( + - {'return '} - {node.argument && renderNode(node.argument, { styleWrapper: styleWrapper})} - {';'} + {'if '} + {'('} + {renderNode(node.test, { styleWrapper: styleWrapper })} + {') '} - ); - }, - BlockStatement(node: StepperBlockStatement) { - return ( - - {'{'} -
+ {renderNode(node.consequent, { styleWrapper: styleWrapper })} + {node.alternate && ( + + {' else '} + {renderNode(node.alternate!, { styleWrapper: styleWrapper })} + + )} + + ); + }, + ReturnStatement(node: StepperReturnStatement) { + return ( + + {'return '} + {node.argument && renderNode(node.argument, { styleWrapper: styleWrapper })} + {';'} + + ); + }, + BlockStatement(node: StepperBlockStatement) { + return ( + + {'{'} +
{node.body.map(ast => (
- {renderNode(ast, { styleWrapper: styleWrapper})} + {renderNode(ast, { styleWrapper: styleWrapper })}
))} +
+ {'}'} +
+ ); + }, + BlockExpression(node: StepperBlockExpression) { + return ( + + {'{'} + {node.body.map(ast => ( +
+ {renderNode(ast, { styleWrapper: styleWrapper })}
- {'}'} -
- ); - }, - BlockExpression(node: StepperBlockExpression) { - return ( - - {'{'} - {node.body.map(ast => ( -
- {renderNode(ast, { styleWrapper: styleWrapper})} -
- ))} - {'};'} -
- ); - }, - ExpressionStatement(node: StepperExpressionStatement) { - return ( - - {renderNode(node.expression, { styleWrapper: styleWrapper})} - {';'} - - ); - }, - FunctionDeclaration(node: StepperFunctionDeclaration) { - return ( - - {`function ${node.id.name}`} - {renderArguments(node.params)} - {" "}{renderNode(node.body, { styleWrapper: styleWrapper})} - - ); - }, - VariableDeclaration(node: StepperVariableDeclaration) { - return ( - - {node.kind} - {node.declarations.map((ast, idx) => ( - - {idx !== 0 && ', '} - {renderNode(ast, { styleWrapper: styleWrapper})} - - ))} - {';'} - - ); - }, - VariableDeclarator(node: StepperVariableDeclarator) { - return ( - - {renderNode(node.id, { styleWrapper: styleWrapper})} - {' = '} - {node.init ? renderNode(node.init, { styleWrapper: styleWrapper}) : 'undefined'} - - ); - } - } + ))} + {'};'} + + ); + }, + ExpressionStatement(node: StepperExpressionStatement) { + return ( + + {renderNode(node.expression, { styleWrapper: styleWrapper })} + {';'} + + ); + }, + FunctionDeclaration(node: StepperFunctionDeclaration) { + return ( + + {`function ${node.id.name}`} + {renderArguments(node.params)} + {renderNode(node.body, { styleWrapper: styleWrapper })} + + ); + }, + VariableDeclaration(node: StepperVariableDeclaration) { + return ( + + {node.kind} + {node.declarations.map((ast, idx) => ( + + {idx !== 0 && ', '} + {renderNode(ast, { styleWrapper: styleWrapper })} + + ))} + {';'} + + ); + }, + VariableDeclarator(node: StepperVariableDeclarator) { + return ( + + {renderNode(node.id, { styleWrapper: styleWrapper })} + {' = '} + {node.init ? renderNode(node.init, { styleWrapper: styleWrapper }) : 'undefined'} + + ); + } + }; // Additional renderers const renderFunctionArguments = (nodes: StepperExpression[]) => { - const args: React.ReactNode[] = nodes.map(arg =>renderNode(arg, { styleWrapper: styleWrapper})); - var renderedArguments = args.slice(1).reduce( + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, { styleWrapper: styleWrapper }) + ); + let renderedArguments = args.slice(1).reduce( (result, item) => ( {result} @@ -529,38 +529,48 @@ function renderNode( }; const renderArguments = (nodes: StepperExpression[]) => { - const args: React.ReactNode[] = nodes.map(arg => - renderNode(arg, {styleWrapper: styleWrapper}) - ); - var renderedArguments = args.slice(1).reduce( - (result, item) => ( - - {result} - {', '} - {item} - - ), - args[0] - ); - renderedArguments = ( + const args: React.ReactNode[] = nodes.map(arg => + renderNode(arg, { styleWrapper: styleWrapper }) + ); + let renderedArguments = args.slice(1).reduce( + (result, item) => ( - {'('} - {renderedArguments} - {')'} + {result} + {', '} + {item} - ); - return renderedArguments; + ), + args[0] + ); + renderedArguments = ( + + {'('} + {renderedArguments} + {')'} + + ); + return renderedArguments; }; // Entry point of rendering const renderer = renderers[currentNode.type as keyof typeof renderers]; - const isParenthesis = expressionNeedsParenthesis(currentNode, renderContext.parentNode, renderContext.isRight); - var result: React.ReactNode = renderer - // @ts-expect-error All subclasses of stepper base node has its corresponding renderes - ? renderer(currentNode) + const isParenthesis = expressionNeedsParenthesis( + currentNode, + renderContext.parentNode, + renderContext.isRight + ); + let result: React.ReactNode = renderer + ? // @ts-expect-error All subclasses of stepper base node has its corresponding renderes + renderer(currentNode) : `<${currentNode.type}>`; // For debugging in case some AST renderer has not been implemented yet if (isParenthesis) { - result = {'('}{result}{')'}; + result = ( + + {'('} + {result} + {')'} + + ); } // custom wrapper style if (styleWrapper) { @@ -574,22 +584,21 @@ function renderNode( * A React component that handles rendering */ function CustomASTRenderer(props: IStepperPropContents): React.ReactNode { - function markerStyleWrapper(node: StepperBaseNode) { - return (renderNode: React.ReactNode) => { - if (props.markers === undefined) { - return renderNode; - } - var returnNode = {renderNode}; - props.markers.forEach(marker => { - if (marker.redex === node) { - returnNode = {returnNode}; - } - }); - return returnNode; - }; - } - const getDisplayedNode = useCallback((): React.ReactNode => { + function markerStyleWrapper(node: StepperBaseNode) { + return (renderNode: React.ReactNode) => { + if (props.markers === undefined) { + return renderNode; + } + let returnNode = {renderNode}; + props.markers.forEach(marker => { + if (marker.redex === node) { + returnNode = {returnNode}; + } + }); + return returnNode; + }; + } return renderNode(props.ast, { styleWrapper: markerStyleWrapper }); @@ -716,5 +725,4 @@ const EXPRESSIONS_PRECEDENCE = { RestElement: 1 }; - export default SideContentSubstVisualizer; diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index 5245fcd156..95aac5c5da 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -48,8 +48,7 @@ export const makeSubstVisualizerTabFrom = ( editorOutput && editorOutput.type === 'result' && editorOutput.value instanceof Array && - editorOutput.value[0] === Object(editorOutput.value[0]) - // && isStepperOutput(editorOutput.value[0]) // FIX: implement isStepperOutput + editorOutput.value[0] === Object(editorOutput.value[0]) ) { return editorOutput.value; } else { diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 6f80530a9d..6f9a200c28 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -422,13 +422,13 @@ $code-color-notification: #f9f0d7; font-family: 'Inconsolata', 'Consolas', monospace; font-size: 16px; } - + .stepper-literal { - color:#FF6078; - } + color: #ff6078; + } .stepper-operator { - color: #F89210; + color: #f89210; } .stepper-identifier { @@ -447,7 +447,7 @@ $code-color-notification: #f9f0d7; .stepper-display { font-family: 'Inconsolata', 'Consolas', monospace; } - + .beforeMarker { display: inline-block; background: rgba(179, 101, 57, 0.75); From 9321d2f70b3bdaf613d30399bc5a14f6b4c1afef Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Sat, 12 Apr 2025 16:19:59 +0800 Subject: [PATCH 08/12] fix: fix parenthesis issue and multiline block expression --- package.json | 3 ++- .../content/SideContentSubstVisualizer.tsx | 25 +++++-------------- src/styles/_workspace.scss | 21 ++++++++++------ 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 82fadd2c5e..760dba8ea5 100644 --- a/package.json +++ b/package.json @@ -177,5 +177,6 @@ "last 1 firefox version", "last 1 safari version" ] - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 423b584c26..07a28b3055 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -20,7 +20,6 @@ import { StepperExpression } from 'js-slang/dist/tracer/nodes'; import { StepperArrayExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrayExpression'; import { StepperArrowFunctionExpression } from 'js-slang/dist/tracer/nodes/Expression/ArrowFunctionExpression'; import { StepperBinaryExpression } from 'js-slang/dist/tracer/nodes/Expression/BinaryExpression'; -import { StepperBlockExpression } from 'js-slang/dist/tracer/nodes/Expression/BlockExpression'; import { StepperConditionalExpression } from 'js-slang/dist/tracer/nodes/Expression/ConditionalExpression'; import { StepperFunctionApplication } from 'js-slang/dist/tracer/nodes/Expression/FunctionApplication'; import { StepperIdentifier } from 'js-slang/dist/tracer/nodes/Expression/Identifier'; @@ -378,7 +377,7 @@ function renderNode(currentNode: StepperBaseNode, renderContext: RenderContext): }, CallExpression(node: StepperFunctionApplication) { let renderedCallee = renderNode(node.callee, { styleWrapper: styleWrapper }); - if (node.callee.type !== 'Identifier') { + if (node.callee.type === 'ArrowFunctionExpression' && node.callee.name === undefined) { renderedCallee = ( {'('} @@ -435,30 +434,18 @@ function renderNode(currentNode: StepperBaseNode, renderContext: RenderContext): return ( {'{'} -
+
{node.body.map(ast => ( -
+ + {renderNode(ast, { styleWrapper: styleWrapper })} -
+
+ ))} -
{'}'}
); }, - BlockExpression(node: StepperBlockExpression) { - return ( - - {'{'} - {node.body.map(ast => ( -
- {renderNode(ast, { styleWrapper: styleWrapper })} -
- ))} - {'};'} -
- ); - }, ExpressionStatement(node: StepperExpressionStatement) { return ( diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 6f9a200c28..9962949649 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -419,8 +419,9 @@ $code-color-notification: #f9f0d7; height: unset; .stepper-display { - font-family: 'Inconsolata', 'Consolas', monospace; - font-size: 16px; + font: 16px / normal 'Inconsolata', + 'Consolas', + monospace; } .stepper-literal { @@ -432,24 +433,26 @@ $code-color-notification: #f9f0d7; } .stepper-identifier { - color: yellow; + color: #F8D871; } .stepper-mu-term { - background: rgba(255, 83, 83, 0.35); pointer-events: auto; cursor: pointer; z-index: 20; padding: 0px 3px; border-radius: 5px; + background: rgba(255, 83, 83, 0.2); } - .stepper-display { - font-family: 'Inconsolata', 'Consolas', monospace; + .stepper-mu-term:hover { + background: rgba(255, 83, 83, 0.5); } .beforeMarker { - display: inline-block; + position: relative; + -webkit-box-decoration-break: slice; + box-decoration-break: slice; background: rgba(179, 101, 57, 0.75); pointer-events: auto; cursor: pointer; @@ -461,7 +464,9 @@ $code-color-notification: #f9f0d7; } .afterMarker { - display: inline-block; + position: relative; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; background: green; pointer-events: auto; cursor: pointer; From b26f3757070335a2974aea2148fbcf2ad3cb79bf Mon Sep 17 00:00:00 2001 From: Kosolpattanadurong Thitiwat Date: Sun, 13 Apr 2025 14:13:56 +0800 Subject: [PATCH 09/12] fix: format fix --- .../content/SideContentSubstVisualizer.tsx | 15 +++++++-------- src/styles/_workspace.scss | 9 +++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index 07a28b3055..d6c7def0f2 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -434,14 +434,13 @@ function renderNode(currentNode: StepperBaseNode, renderContext: RenderContext): return ( {'{'} -
- {node.body.map(ast => ( - - - {renderNode(ast, { styleWrapper: styleWrapper })} -
-
- ))} +
+ {node.body.map(ast => ( + + {renderNode(ast, { styleWrapper: styleWrapper })} +
+
+ ))} {'}'}
); diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 9962949649..df4cbddc40 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -419,9 +419,10 @@ $code-color-notification: #f9f0d7; height: unset; .stepper-display { - font: 16px / normal 'Inconsolata', - 'Consolas', - monospace; + font: + 16px / normal 'Inconsolata', + 'Consolas', + monospace; } .stepper-literal { @@ -433,7 +434,7 @@ $code-color-notification: #f9f0d7; } .stepper-identifier { - color: #F8D871; + color: #f8d871; } .stepper-mu-term { From b099d440e466fbf86d857e76d0b3f2b814235ea9 Mon Sep 17 00:00:00 2001 From: Thitiwat Kosolpattanadurong <167766536+CATISNOTSODIUM@users.noreply.github.com> Date: Tue, 15 Apr 2025 10:54:05 +0000 Subject: [PATCH 10/12] fix: add logical expression handler --- .../sideContent/content/SideContentSubstVisualizer.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index d6c7def0f2..c69738efee 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -41,6 +41,7 @@ import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; +import { StepperLogicalExpression } from 'js-slang/dist/tracer/nodes/Expression/LogicalExpression'; const SubstDefaultText = () => { return ( @@ -272,6 +273,15 @@ function renderNode(currentNode: StepperBaseNode, renderContext: RenderContext):
); }, + LogicalExpression(node: StepperLogicalExpression) { + return ( + + {renderNode(node.left, { parentNode: node, isRight: false, styleWrapper: styleWrapper })} + {` ${node.operator} `} + {renderNode(node.right, { parentNode: node, isRight: true, styleWrapper: styleWrapper })} + + ); + }, ConditionalExpression(node: StepperConditionalExpression) { return ( From f470d9c169a70126ca933e9b0668f32857837676 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 18 Apr 2025 03:19:43 +0800 Subject: [PATCH 11/12] Fix incorrect merge conflict resolution --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 21137dea4f..f89f8c35f4 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } } From 01b3318199789d5652a229f1f6ea4fbc33ec7870 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 18 Apr 2025 03:20:40 +0800 Subject: [PATCH 12/12] Fix lint --- src/commons/sideContent/content/SideContentSubstVisualizer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx index c69738efee..1410008fed 100644 --- a/src/commons/sideContent/content/SideContentSubstVisualizer.tsx +++ b/src/commons/sideContent/content/SideContentSubstVisualizer.tsx @@ -24,6 +24,7 @@ import { StepperConditionalExpression } from 'js-slang/dist/tracer/nodes/Express import { StepperFunctionApplication } from 'js-slang/dist/tracer/nodes/Expression/FunctionApplication'; import { StepperIdentifier } from 'js-slang/dist/tracer/nodes/Expression/Identifier'; import { StepperLiteral } from 'js-slang/dist/tracer/nodes/Expression/Literal'; +import { StepperLogicalExpression } from 'js-slang/dist/tracer/nodes/Expression/LogicalExpression'; import { StepperUnaryExpression } from 'js-slang/dist/tracer/nodes/Expression/UnaryExpression'; import { StepperProgram } from 'js-slang/dist/tracer/nodes/Program'; import { StepperBlockStatement } from 'js-slang/dist/tracer/nodes/Statement/BlockStatement'; @@ -41,7 +42,6 @@ import { useDispatch } from 'react-redux'; import { beginAlertSideContent } from '../SideContentActions'; import { SideContentLocation, SideContentType } from '../SideContentTypes'; -import { StepperLogicalExpression } from 'js-slang/dist/tracer/nodes/Expression/LogicalExpression'; const SubstDefaultText = () => { return (