From 2a0257312245f909d7133541efa2b39492a119a9 Mon Sep 17 00:00:00 2001 From: xyliew25 Date: Wed, 10 Apr 2024 22:52:35 +0800 Subject: [PATCH 01/12] Update ControlBarChapterSelect with Java --- src/commons/controlBar/ControlBarChapterSelect.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commons/controlBar/ControlBarChapterSelect.tsx b/src/commons/controlBar/ControlBarChapterSelect.tsx index aad0e84e19..47ba376344 100644 --- a/src/commons/controlBar/ControlBarChapterSelect.tsx +++ b/src/commons/controlBar/ControlBarChapterSelect.tsx @@ -9,6 +9,7 @@ import { fullJSLanguage, fullTSLanguage, htmlLanguage, + javaLanguages, pyLanguages, SALanguage, schemeLanguages, @@ -87,7 +88,8 @@ export const ControlBarChapterSelect: React.FC = ( // See https://github.com/source-academy/frontend/pull/2460#issuecomment-1528759912 ...(Constants.playgroundOnly ? [fullJSLanguage, fullTSLanguage, htmlLanguage] : []), ...schemeLanguages, - ...pyLanguages + ...pyLanguages, + ...javaLanguages, ]; return ( From bb1f2b3f43d86c9cb439d345fbff37af7769d111 Mon Sep 17 00:00:00 2001 From: xyliew25 Date: Wed, 10 Apr 2024 22:54:13 +0800 Subject: [PATCH 02/12] Implement Java CSEC Visualizer --- src/features/cseMachine/java/CseMachine.tsx | 155 ++++++++++++++ .../cseMachine/java/components/Arrow.tsx | 72 +++++++ .../cseMachine/java/components/Binding.tsx | 98 +++++++++ .../cseMachine/java/components/Control.tsx | 202 ++++++++++++++++++ .../java/components/ControlItem.tsx | 154 +++++++++++++ .../java/components/Environment.tsx | 189 ++++++++++++++++ .../cseMachine/java/components/Frame.tsx | 150 +++++++++++++ .../cseMachine/java/components/Line.tsx | 62 ++++++ .../cseMachine/java/components/Method.tsx | 119 +++++++++++ .../cseMachine/java/components/Object.tsx | 44 ++++ .../cseMachine/java/components/Stash.tsx | 90 ++++++++ .../cseMachine/java/components/StashItem.tsx | 95 ++++++++ .../cseMachine/java/components/Text.tsx | 60 ++++++ .../cseMachine/java/components/Variable.tsx | 109 ++++++++++ 14 files changed, 1599 insertions(+) create mode 100644 src/features/cseMachine/java/CseMachine.tsx create mode 100644 src/features/cseMachine/java/components/Arrow.tsx create mode 100644 src/features/cseMachine/java/components/Binding.tsx create mode 100644 src/features/cseMachine/java/components/Control.tsx create mode 100644 src/features/cseMachine/java/components/ControlItem.tsx create mode 100644 src/features/cseMachine/java/components/Environment.tsx create mode 100644 src/features/cseMachine/java/components/Frame.tsx create mode 100644 src/features/cseMachine/java/components/Line.tsx create mode 100644 src/features/cseMachine/java/components/Method.tsx create mode 100644 src/features/cseMachine/java/components/Object.tsx create mode 100644 src/features/cseMachine/java/components/Stash.tsx create mode 100644 src/features/cseMachine/java/components/StashItem.tsx create mode 100644 src/features/cseMachine/java/components/Text.tsx create mode 100644 src/features/cseMachine/java/components/Variable.tsx diff --git a/src/features/cseMachine/java/CseMachine.tsx b/src/features/cseMachine/java/CseMachine.tsx new file mode 100644 index 0000000000..9893220420 --- /dev/null +++ b/src/features/cseMachine/java/CseMachine.tsx @@ -0,0 +1,155 @@ +import { Context } from "java-slang/dist/ec-evaluator/types"; +import { KonvaEventObject } from "konva/lib/Node"; +import React, { RefObject } from "react"; +import { Layer, Rect, Stage } from "react-konva"; + +import { Config, ShapeDefaultProps } from "./../CseMachineConfig"; +import { Control } from "./components/Control"; +import { Environment } from "./components/Environment"; +import { Stash } from "./components/Stash"; + +type SetVis = (vis: React.ReactNode) => void; +type SetEditorHighlightedLines = (segments: [number, number][]) => void; + +export class CseMachine { + /** the unique key assigned to each node */ + static key: number = 0; + + /** callback function to update the visualization state in the SideContentCseMachine component */ + private static setVis: SetVis; + /** function to highlight editor lines */ + public static setEditorHighlightedLines: SetEditorHighlightedLines; + + public static stageRef: RefObject = React.createRef(); + /** scale factor for zooming and out of canvas */ + public static scaleFactor = 1.02; + + static environment: Environment | undefined; + static control: Control | undefined; + static stash: Stash | undefined; + + static init( + setVis: SetVis, + setEditorHighlightedLines: (segments: [number, number][]) => void, + ) { + this.setVis = setVis; + this.setEditorHighlightedLines = setEditorHighlightedLines; + } + + /** updates the visualization state in the SideContentCseMachine component based on + * the Java Slang context passed in */ + static drawCse(context: Context) { + if (!this.setVis || !context.environment || !context.control || !context.stash) { + throw new Error('Java CSE Machine not initialized'); + } + + CseMachine.environment = new Environment(context.environment); + CseMachine.control = new Control(context.control); + CseMachine.stash = new Stash(context.stash); + + this.setVis(this.draw()); + + // Set icon to blink. + const icon = document.getElementById('env_visualizer-icon'); + icon && icon.classList.add('side-content-tab-alert'); + } + + static clearCse() { + if (this.setVis) { + this.setVis(undefined); + CseMachine.environment = undefined; + CseMachine.control = undefined; + CseMachine.stash = undefined; + } + } + + /** + * Updates the scale of the stage after the user inititates a zoom in or out + * by scrolling or by the trackpad. + */ + static zoomStage(event: KonvaEventObject | boolean, multiplier: number = 1) { + typeof event != 'boolean' && event.evt.preventDefault(); + if (CseMachine.stageRef.current) { + const stage = CseMachine.stageRef.current; + const oldScale = stage.scaleX(); + const { x: pointerX, y: pointerY } = stage.getPointerPosition(); + const mousePointTo = { + x: (pointerX - stage.x()) / oldScale, + y: (pointerY - stage.y()) / oldScale + }; + + // zoom in or zoom out + const direction = + typeof event != 'boolean' ? (event.evt.deltaY > 0 ? -1 : 1) : event ? 1 : -1; + + // Check if the zoom limits have been reached + if ((direction > 0 && oldScale < 3) || (direction < 0 && oldScale > 0.4)) { + const newScale = + direction > 0 + ? oldScale * CseMachine.scaleFactor ** multiplier + : oldScale / CseMachine.scaleFactor ** multiplier; + stage.scale({ x: newScale, y: newScale }); + if (typeof event !== 'boolean') { + const newPos = { + x: pointerX - mousePointTo.x * newScale, + y: pointerY - mousePointTo.y * newScale + }; + stage.position(newPos); + stage.batchDraw(); + } + } + } + } + + static draw(): React.ReactNode { + const layout = ( +
+
+
+ + + + {this.control?.draw()} + {this.stash?.draw()} + {this.environment?.draw()} + + +
+
+
+ ); + + return layout; + } +} diff --git a/src/features/cseMachine/java/components/Arrow.tsx b/src/features/cseMachine/java/components/Arrow.tsx new file mode 100644 index 0000000000..51de860418 --- /dev/null +++ b/src/features/cseMachine/java/components/Arrow.tsx @@ -0,0 +1,72 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import { Arrow as KonvaArrow, Group as KonvaGroup, Path as KonvaPath } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + setHoveredCursor, + setHoveredStyle, + setUnhoveredCursor, + setUnhoveredStyle, +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +/** this class encapsulates an Arrow to be drawn between 2 points */ +export class Arrow extends Visible implements IHoverable { + private static readonly TO_X_INDEX = 2; + private readonly _points: number[] = []; + + constructor( + fromX: number, + fromY: number, + toX: number, + toY: number, + ) { + super(); + this._points.push(fromX, fromY, toX, toY); + } + + setToX(x: number) { + this._points[Arrow.TO_X_INDEX] = x; + } + + onMouseEnter(e: KonvaEventObject) { + setHoveredStyle(e.currentTarget) + setHoveredCursor(e.currentTarget); + } + + onMouseLeave(e: KonvaEventObject) { + setUnhoveredStyle(e.currentTarget); + setUnhoveredCursor(e.currentTarget); + } + + draw() { + const path = `M ${this._points[0]} ${this._points[1]} L ${this._points[2]} ${this._points[3]}`; + return ( + this.onMouseEnter(e)} + onMouseLeave={e => this.onMouseLeave(e)} + > + + + + ); + } +} diff --git a/src/features/cseMachine/java/components/Binding.tsx b/src/features/cseMachine/java/components/Binding.tsx new file mode 100644 index 0000000000..2a20032c9e --- /dev/null +++ b/src/features/cseMachine/java/components/Binding.tsx @@ -0,0 +1,98 @@ +import { Name, StructType, Value } from 'java-slang/dist/ec-evaluator/types'; +import React from 'react'; + +import { Visible } from '../../components/Visible'; +import { Config } from '../../CseMachineConfig'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Method } from './Method'; +import { Text } from './Text'; +import { Variable } from './Variable'; + +/** a Binding is a key-value pair in a Frame */ +export class Binding extends Visible { + private readonly _name: Text; + + private readonly _value: Variable | Method | Text; + // Only Method has arrow. + private readonly _arrow: Arrow | undefined; + + constructor( + name: Name, + value: Value, + x: number, + y: number + ) { + super(); + + // Position. + this._x = x; + this._y = y; + + if (value.kind === StructType.CLOSURE) { + // Name. + this._name = new Text( + name + Config.VariableColon, // := is part of name + this.x(), + this.y()); + // Value. + this._value = new Method( + this._name.x() + this._name.width(), + this.y() + this._name.height() / 2, + value); + this._arrow = new Arrow( + this._name.x() + this._name.width(), + this._name.y() + this._name.height() / 2, + this._value.x(), + this._value.y()); + } else if (value.kind === StructType.VARIABLE) { + // Name. + this._name = new Text( + name + Config.VariableColon, // := is part of name + this.x(), + this.y() + Config.FontSize + Config.TextPaddingX); + // Value. + this._value = new Variable( + this._name.x() + this._name.width(), + this.y(), + value); + } else /*if (value.kind === StructType.CLASS)*/ { + // Dummy value as class will nvr be drawn. + // Name. + this._name = new Text( + name + Config.VariableColon, // := is part of name + this.x(), + this.y() + Config.FontSize + Config.TextPaddingX); + // Value. + this._value = new Text( + "", + this._name.x() + this._name.width() + Config.TextPaddingX, + this.y() + Config.TextPaddingX); + } + + // Height and width. + this._height = Math.max(this._name.height(), this._value.height()); + this._width = this._value.x() + this._value.width() - this._name.x(); + } + + get value() { + return this._value; + } + + setArrowToX(x: number) { + this._arrow?.setToX(x); + } + + draw(): React.ReactNode { + return ( + + {/* Name */} + {this._name.draw()} + + {/* Value */} + {this._value.draw()} + {this._arrow?.draw()} + + ); + } +} diff --git a/src/features/cseMachine/java/components/Control.tsx b/src/features/cseMachine/java/components/Control.tsx new file mode 100644 index 0000000000..7d2a7706bc --- /dev/null +++ b/src/features/cseMachine/java/components/Control.tsx @@ -0,0 +1,202 @@ +import { astToString } from "java-slang/dist/ast/utils/astToString"; +import { Control as JavaControl } from "java-slang/dist/ec-evaluator/components"; +import { + BinOpInstr, + ControlItem as JavaControlItem, + EnvInstr, + EvalVarInstr, + InstrType, + InvInstr, + NewInstr, + ResConOverloadInstr, + ResInstr, + ResOverloadInstr, + ResTypeContInstr, + ResTypeInstr, +} from "java-slang/dist/ec-evaluator/types"; +import { isInstr, isNode } from "java-slang/dist/ec-evaluator/utils"; +import { Group } from "react-konva"; + +import { Visible } from "../../components/Visible"; +import { Config } from "../../CseMachineConfig"; +import { ControlStashConfig } from "../../CseMachineControlStashConfig"; +import { CseMachine } from "../CseMachine"; +import { ControlItem } from "./ControlItem"; + +export class Control extends Visible { + private readonly _controlItems: ControlItem[] = []; + + constructor(control: JavaControl) { + super(); + + // Position. + this._x = ControlStashConfig.ControlPosX; + this._y = ControlStashConfig.ControlPosY + ControlStashConfig.StashItemHeight + ControlStashConfig.StashItemTextPadding * 2; + + // Create each ControlItem. + let controlItemY: number = this._y; + control.getStack().forEach((controlItem, index) => { + const controlItemText = this.getControlItemString(controlItem); + + const controlItemStroke = + index === control.getStack().length - 1 + ? Config.SA_CURRENT_ITEM + : ControlStashConfig.SA_WHITE; + + // TODO reference draw ltr? + const controlItemReference = + isInstr(controlItem) && controlItem.instrType === InstrType.ENV + ? CseMachine.environment?.frames.find(f => f.frame === (controlItem as EnvInstr).env) + : undefined; + + const controlItemTooltip = this.getControlItemTooltip(controlItem); + this.getControlItemTooltip(controlItem); + + const node = isNode(controlItem) ? controlItem : controlItem.srcNode; + const highlightOnHover = () => { + let start = -1; + let end = -1; + if (node.location) { + start = node.location.startLine - 1; + end = node.location.endLine ? node.location.endLine - 1 : start; + } + CseMachine.setEditorHighlightedLines([[start, end]]); + }; + const unhighlightOnHover = () => CseMachine.setEditorHighlightedLines([]); + + const currControlItem = new ControlItem( + controlItemY, + controlItemText, + controlItemStroke, + controlItemReference, + controlItemTooltip, + highlightOnHover, + unhighlightOnHover, + ); + + this._controlItems.push(currControlItem); + controlItemY += currControlItem.height(); + }); + + // Height and width. + this._height = controlItemY - this._y; + // TODO cal real width? + this._width = ControlStashConfig.ControlItemWidth; + } + + draw(): React.ReactNode { + return ( + + {this._controlItems.map(c => c.draw())} + + ); + } + + private getControlItemString = (controlItem: JavaControlItem): string => { + if (isNode(controlItem)) { + return astToString(controlItem); + } + + switch (controlItem.instrType) { + case InstrType.RESET: + return "return"; + case InstrType.ASSIGNMENT: + return "asgn"; + case InstrType.BINARY_OP: + const binOpInstr = controlItem as BinOpInstr; + return binOpInstr.symbol; + case InstrType.POP: + return "pop"; + case InstrType.INVOCATION: + const appInstr = controlItem as InvInstr; + return `invoke ${appInstr.arity}`; + case InstrType.ENV: + return "env"; + case InstrType.MARKER: + return "mark"; + case InstrType.EVAL_VAR: + const evalVarInstr = controlItem as EvalVarInstr; + return `name ${evalVarInstr.symbol}`; + case InstrType.NEW: + const newInstr = controlItem as NewInstr; + return `new ${newInstr.c.frame.name}`; + case InstrType.RES_TYPE: + const resTypeInstr = controlItem as ResTypeInstr; + return `res_type ${resTypeInstr.value.kind === "Class" + ? resTypeInstr.value.frame.name : + astToString(resTypeInstr.value)}`; + case InstrType.RES_TYPE_CONT: + const resTypeContInstr = controlItem as ResTypeContInstr; + return `res_type_cont ${resTypeContInstr.name}`; + case InstrType.RES_OVERLOAD: + const resOverloadInstr = controlItem as ResOverloadInstr; + return `res_overload ${resOverloadInstr.name} ${resOverloadInstr.arity}`; + case InstrType.RES_OVERRIDE: + return `res_override`; + case InstrType.RES_CON_OVERLOAD: + const resConOverloadInstr = controlItem as ResConOverloadInstr; + return `res_con_overload ${resConOverloadInstr.arity}`; + case InstrType.RES: + const resInstr = controlItem as ResInstr; + return `res ${resInstr.name}`; + case InstrType.DEREF: + return "deref"; + default: + return "INSTRUCTION"; + } + } + + private getControlItemTooltip = (controlItem: JavaControlItem): string => { + if (isNode(controlItem)) { + return astToString(controlItem); + } + + switch (controlItem.instrType) { + case InstrType.RESET: + return "Skip control items until marker instruction is reached"; + case InstrType.ASSIGNMENT: + return "Assign value on top of stash to location on top of stash"; + case InstrType.BINARY_OP: + const binOpInstr = controlItem as BinOpInstr; + return `Perform ${binOpInstr.symbol} on top 2 stash values`; + case InstrType.POP: + return "Pop most recently pushed value from stash"; + case InstrType.INVOCATION: + const appInstr = controlItem as InvInstr; + return `Invoke method with ${appInstr.arity} argument${appInstr.arity === 1 ? '' : 's'}`; + case InstrType.ENV: + return "Set current environment to this environment"; + case InstrType.MARKER: + return "Mark return address"; + case InstrType.EVAL_VAR: + const evalVarInstr = controlItem as EvalVarInstr; + return `name ${evalVarInstr.symbol}`; + case InstrType.NEW: + const newInstr = controlItem as NewInstr; + return `Create new instance of class ${newInstr.c.frame.name}`; + case InstrType.RES_TYPE: + const resTypeInstr = controlItem as ResTypeInstr; + return `Resolve type of ${resTypeInstr.value.kind === "Class" + ? resTypeInstr.value.frame.name : + astToString(resTypeInstr.value)}`; + case InstrType.RES_TYPE_CONT: + const resTypeContInstr = controlItem as ResTypeContInstr; + return `Resolve type of ${resTypeContInstr.name} in most recently pushed type from stash`; + case InstrType.RES_OVERLOAD: + const resOverloadInstr = controlItem as ResOverloadInstr; + return `Resolve overloading of method ${resOverloadInstr.name} with ${resOverloadInstr.arity} argument${resOverloadInstr.arity === 1 ? '' : 's'}`; + case InstrType.RES_OVERRIDE: + return "Resolve overriding of resolved method on top of stash"; + case InstrType.RES_CON_OVERLOAD: + const resConOverloadInstr = controlItem as ResConOverloadInstr; + return `Resolve constructor overloading of class on stash with ${resConOverloadInstr.arity} argument${resConOverloadInstr.arity === 1 ? '' : 's'}`; + case InstrType.RES: + const resInstr = controlItem as ResInstr; + return `Resolve field ${resInstr.name} of most recently pushed value from stash`; + case InstrType.DEREF: + return "Dereference most recently pushed value from stash"; + default: + return "INSTRUCTION"; + } + } +} diff --git a/src/features/cseMachine/java/components/ControlItem.tsx b/src/features/cseMachine/java/components/ControlItem.tsx new file mode 100644 index 0000000000..d50dba4d29 --- /dev/null +++ b/src/features/cseMachine/java/components/ControlItem.tsx @@ -0,0 +1,154 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Label, Tag, Text } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + getTextHeight, + setHoveredCursor, + setHoveredStyle, + setUnhoveredCursor, + setUnhoveredStyle, + truncateText, +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Frame } from './Frame'; + +export class ControlItem extends Visible implements IHoverable { + private readonly _arrow: Arrow | undefined; + private readonly _tooltipRef: RefObject; + + constructor( + y: number, + + private readonly _text: string, + private readonly _stroke: string, + + reference: Frame | undefined, + + private readonly _tooltip: string, + private readonly highlightOnHover: () => void, + private readonly unhighlightOnHover: () => void, + ) { + super(); + + // Position. + this._x = ControlStashConfig.ControlPosX; + this._y = y; + + // Text. + this._text = truncateText( + this._text, + ControlStashConfig.ControlMaxTextWidth, + ControlStashConfig.ControlMaxTextHeight + ); + + // Tooltip. + this._tooltipRef = React.createRef(); + + // Height and width. + this._height = getTextHeight(this._text, ControlStashConfig.ControlMaxTextWidth) + + ControlStashConfig.ControlItemTextPadding * 2; + this._width = ControlStashConfig.ControlItemWidth; + + // Arrow + if (reference) { + this._arrow = new Arrow( + this._x + this._width, + this._y + this._height / 2, + reference.x(), + reference.y() + reference.height() / 2 + reference.name.height(), + ); + } + } + + private isCurrentItem = (): boolean => { + return this._stroke === Config.SA_CURRENT_ITEM; + } + + onMouseEnter = (e: KonvaEventObject): void => { + this.highlightOnHover(); + !this.isCurrentItem() && setHoveredStyle(e.currentTarget); + setHoveredCursor(e.currentTarget); + this._tooltipRef.current.show(); + }; + + onMouseLeave = (e: KonvaEventObject): void => { + this.unhighlightOnHover(); + !this.isCurrentItem() && setUnhoveredStyle(e.currentTarget); + setUnhoveredCursor(e.currentTarget); + this._tooltipRef.current.hide(); + }; + + draw(): React.ReactNode { + const textProps = { + fill: ControlStashConfig.SA_WHITE, + padding: ControlStashConfig.ControlItemTextPadding, + fontFamily: ControlStashConfig.FontFamily, + fontSize: ControlStashConfig.FontSize, + fontStyle: ControlStashConfig.FontStyle, + fontVariant: ControlStashConfig.FontVariant, + }; + const tagProps = { + stroke: this._stroke, + cornerRadius: ControlStashConfig.ControlItemCornerRadius, + }; + return ( + + {/* Text */} + + + {/* Tooltip */} + + + {/* Arrow */} + {this._arrow?.draw()} + + ); + } +} diff --git a/src/features/cseMachine/java/components/Environment.tsx b/src/features/cseMachine/java/components/Environment.tsx new file mode 100644 index 0000000000..0e456c170d --- /dev/null +++ b/src/features/cseMachine/java/components/Environment.tsx @@ -0,0 +1,189 @@ +import { Environment as JavaEnvironment, EnvNode } from "java-slang/dist/ec-evaluator/components"; +import { Class as JavaClass, StructType } from "java-slang/dist/ec-evaluator/types"; +import { Group } from "react-konva"; + +import { Visible } from "../../components/Visible"; +import { Config } from "../../CseMachineConfig"; +import { ControlStashConfig } from "../../CseMachineControlStashConfig"; +import { CseMachine } from "../CseMachine"; +import { Arrow } from "./Arrow"; +import { Frame } from "./Frame"; +import { Line } from "./Line"; +import { Object } from "./Object"; +import { Variable } from "./Variable"; + +export class Environment extends Visible { + private readonly _methodFrames: Frame[] = []; + private readonly _objects: Object[] = []; + private readonly _classFrames: Frame[] = []; + private readonly _lines: Line[] = []; + + constructor(environment: JavaEnvironment) { + super(); + + // Position. + this._x = ControlStashConfig.ControlPosX + ControlStashConfig.ControlItemWidth + 2 * Config.CanvasPaddingX; + this._y = ControlStashConfig.StashPosY + ControlStashConfig.StashItemHeight + 2 * Config.CanvasPaddingY; + + // Create method frames. + const methodFramesX = this._x; + let methodFramesY: number = this._y; + let methodFramesWidth = Number(Config.FrameMinWidth); + environment.global.children.forEach(env => { + if (env.name.includes("(")) { + let currEnv: EnvNode | undefined = env; + let parentFrame; + while (currEnv) { + const stroke = currEnv === environment.current ? Config.SA_CURRENT_ITEM : Config.SA_WHITE; + const frame = new Frame(currEnv, methodFramesX, methodFramesY, stroke); + this._methodFrames.push(frame); + methodFramesY += (frame.height() + Config.FramePaddingY); + methodFramesWidth = Math.max(methodFramesWidth, frame.width()); + parentFrame && frame.setParent(parentFrame); + + parentFrame = frame; + currEnv = currEnv.children.length ? currEnv.children[0] : undefined; + } + } + }); + + // Create objects. + const objectFramesX = methodFramesX + methodFramesWidth + Config.FrameMinWidth; + let objectFramesY: number = this._y; + let objectFramesWidth = Number(Config.FrameMinWidth); + environment.objects.forEach(obj => { + const objectFrames: Frame[] = []; + let objectFrameWidth = Number(Config.FrameMinWidth); + + // Get top env. + let env: EnvNode | undefined = obj.frame; + while (env.parent) { + env = env.parent; + } + + // Create frame top-down. + while (env) { + const stroke = env === environment.current ? Config.SA_CURRENT_ITEM : Config.SA_WHITE; + const frame = new Frame(env, objectFramesX, objectFramesY, stroke); + // No padding btwn obj frames thus no arrows required. + objectFramesY += frame.height(); + objectFramesWidth = Math.max(objectFramesWidth, frame.width()); + + env = env.children.length ? env.children[0] : undefined; + + objectFrames.push(frame); + objectFrameWidth = Math.max(objectFrameWidth, frame.width()); + } + + // Standardize obj frames width. + objectFrames.forEach(o => o.setWidth(objectFrameWidth)) + + // Only add padding btwn objects. + objectFramesY += Config.FramePaddingY + + this._objects.push(new Object(objectFrames, obj)); + }); + + // Create class frames. + const classFramesX = objectFramesX + objectFramesWidth + Config.FrameMinWidth; + let classFramesY = this._y; + for (const [_, c] of environment.global.frame.entries()) { + const classEnv = (c as JavaClass).frame; + const classFrameStroke = classEnv === environment.current ? Config.SA_CURRENT_ITEM : Config.SA_WHITE; + const highlightOnHover = () => { + const node = (c as JavaClass).classDecl; + let start = -1; + let end = -1; + if (node.location) { + start = node.location.startLine - 1; + end = node.location.endLine ? node.location.endLine - 1 : start; + } + CseMachine.setEditorHighlightedLines([[start, end]]); + }; + const unhighlightOnHover = () => CseMachine.setEditorHighlightedLines([]); + const classFrame = new Frame(classEnv, classFramesX, classFramesY, classFrameStroke, "", highlightOnHover, unhighlightOnHover); + const superClassName = (c as JavaClass).superclass?.frame.name; + if (superClassName) { + const parentFrame = this._classFrames.find(f => f.name.text === superClassName)!; + classFrame.setParent(parentFrame) + } + this._classFrames.push(classFrame); + classFramesY += (classFrame.height() + Config.FramePaddingY); + } + + // Draw arrow for var ref in mtd frames to corresponding obj. + this._methodFrames.forEach(mf => { + mf.bindings.forEach(b => { + if (b.value instanceof Variable && b.value.variable.value.kind === StructType.OBJECT) { + const objFrame = b.value.variable.value.frame; + const matchingObj = this._objects.filter(o => o.getFrame().frame === objFrame)[0]; + b.value.value = new Arrow( + b.value.x() + b.value.width() / 2, + b.value.y() + b.value.type.height() + (b.value.height() - b.value.type.height()) / 2, + matchingObj.x(), + matchingObj.y() + matchingObj.height() / 2 + ); + } + }) + }); + + // Draw arrow for var ref in obj frames to corresponding var or obj. + this._objects.flatMap(obj => obj.frames).forEach(of => { + of.bindings.forEach(b => { + if (b.value instanceof Variable && b.value.variable.value.kind === StructType.VARIABLE) { + const variable = b.value.variable.value; + const matchingVariable = this._classFrames.flatMap(c => c.bindings).filter(b => b.value instanceof Variable && b.value.variable === variable)[0].value as Variable; + b.value.value = new Arrow( + b.value.x() + b.value.width() / 2, + b.value.y() + b.value.type.height() + (b.value.height() - b.value.type.height()) / 2, + matchingVariable.x(), + matchingVariable.y() + matchingVariable.type.height()); + } + if (b.value instanceof Variable && b.value.variable.value.kind === StructType.OBJECT) { + const obj = b.value.variable.value.frame; + const matchingObj = this._objects.find(o => o.getFrame().frame === obj)!; + // Variable always has a box. + b.value.value = new Arrow( + b.value.x() + b.value.width() / 2, + b.value.y() + b.value.type.height() + (b.value.height() - b.value.type.height()) / 2, + matchingObj.x(), + matchingObj.y() + matchingObj.height() / 2); + } + }) + }); + + // Draw line for obj to class. + this._objects.forEach(obj => { + const matchingClass = this._classFrames.find(c => c.name.text === obj.object.class.frame.name)!; + const line = new Line( + obj.x() + obj.width(), + obj.y() + obj.height() / 2, + matchingClass.x(), + matchingClass.y() + matchingClass.height() / 2 + matchingClass.name.height()); + this._lines.push(line); + }); + } + + get classes() { + return this._classFrames; + } + + get objects() { + return this._objects.flatMap(obj => obj.frames); + } + + get frames() { + return this._methodFrames; + } + + draw(): React.ReactNode { + return ( + + {this._methodFrames.map(f => f.draw())} + {this._objects.flatMap(obj => obj.frames).map(f => f.draw())} + {this._classFrames.map(f => f.draw())} + {this._lines.map(f => f.draw())} + + ); + } +} diff --git a/src/features/cseMachine/java/components/Frame.tsx b/src/features/cseMachine/java/components/Frame.tsx new file mode 100644 index 0000000000..975e07e79e --- /dev/null +++ b/src/features/cseMachine/java/components/Frame.tsx @@ -0,0 +1,150 @@ +import { EnvNode } from 'java-slang/dist/ec-evaluator/components'; +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Group, Label, Rect, Tag, Text as KonvaText } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { setHoveredCursor, setUnhoveredCursor } from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Binding } from './Binding'; +import { Method } from './Method'; +import { Text } from './Text'; + +export class Frame extends Visible implements IHoverable { + readonly tooltipRef: RefObject; + + readonly bindings: Binding[] = []; + readonly name: Text; + private parent: Frame | undefined; + + constructor( + readonly frame: EnvNode, + x: number, + y: number, + readonly stroke: string, + + readonly tooltip?: string, + readonly highlightOnHover?: () => void, + readonly unhighlightOnHover?: () => void, + ) { + super(); + + this._x = x; + this._y = y; + + this.name = new Text(frame.name, this._x + Config.FramePaddingX, this._y); + + this._width = Math.max(Config.FrameMinWidth, this.name.width() + 2 * Config.FramePaddingX); + this._height = Config.FramePaddingY + this.name.height(); + + // Create binding for each key-value pair + let bindingY: number = this._y + this.name.height() + Config.FramePaddingY; + for (const [key, data] of frame.frame) { + const currBinding: Binding = new Binding(key, data, this._x + Config.FramePaddingX, bindingY); + this.bindings.push(currBinding); + bindingY += (currBinding.height() + Config.FramePaddingY); + this._width = Math.max(this._width, currBinding.width() + 2 * Config.FramePaddingX); + this._height += (currBinding.height() + Config.FramePaddingY); + } + + // Set x of Method aft knowing frame width. + this.bindings.filter(b => b.value instanceof Method).forEach(b => { + (b.value as Method).setX(this._x + this._width + Config.FramePaddingX); + b.setArrowToX(this._x + this._width + Config.FramePaddingX); + }) + + this.tooltipRef = React.createRef(); + } + + setWidth(width: number) { + this._width = width; + } + + setParent(parent: Frame) { + this.parent = parent; + } + + onMouseEnter = (e: KonvaEventObject) => { + this.highlightOnHover && this.highlightOnHover(); + (this.tooltip || this.highlightOnHover) && setHoveredCursor(e.currentTarget); + this.tooltip && this.tooltipRef.current.show(); + }; + + onMouseLeave = (e: KonvaEventObject) => { + this.unhighlightOnHover && this.unhighlightOnHover(); + (this.tooltip || this.unhighlightOnHover) && setUnhoveredCursor(e.currentTarget); + this.tooltip && this.tooltipRef.current.hide(); + }; + + draw(): React.ReactNode { + const textProps = { + fill: ControlStashConfig.SA_WHITE.toString(), + padding: Number(ControlStashConfig.ControlItemTextPadding), + fontFamily: ControlStashConfig.FontFamily.toString(), + fontSize: Number(ControlStashConfig.FontSize), + fontStyle: ControlStashConfig.FontStyle.toString(), + fontVariant: ControlStashConfig.FontVariant.toString() + }; + + return ( + + + {/* Frame name */} + {this.name.draw()} + + {/* Frame */} + {this.bindings.map(binding => binding.draw())} + + {/* Frame parent */} + {this.parent && new Arrow( + this._x + Config.FramePaddingX / 2, + this._y + this.name.height(), + this.parent.x() + Config.FramePaddingX / 2, + // TODO WHY NEED TO ADD NAME HEIGHT? + this.parent.y() + this.parent.height() + this.name?.height() + ).draw()} + + {/* Frame tooltip */} + {this.tooltip && + + } + + ); + } +} diff --git a/src/features/cseMachine/java/components/Line.tsx b/src/features/cseMachine/java/components/Line.tsx new file mode 100644 index 0000000000..ec462a6846 --- /dev/null +++ b/src/features/cseMachine/java/components/Line.tsx @@ -0,0 +1,62 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import { Group, Path } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + setHoveredCursor, + setHoveredStyle, + setUnhoveredCursor, + setUnhoveredStyle, +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +/** this class encapsulates a Line to be drawn between 2 points */ +export class Line extends Visible implements IHoverable { + private static readonly TO_X_INDEX = 2; + private readonly _points: number[] = []; + + constructor( + fromX: number, + fromY: number, + toX: number, + toY: number, + ) { + super(); + this._points.push(fromX, fromY, toX, toY); + } + + setToX(x: number) { + this._points[Line.TO_X_INDEX] = x; + } + + onMouseEnter(e: KonvaEventObject) { + setHoveredStyle(e.currentTarget) + setHoveredCursor(e.currentTarget); + } + + onMouseLeave(e: KonvaEventObject) { + setUnhoveredStyle(e.currentTarget) + setUnhoveredCursor(e.currentTarget); + } + + draw() { + const path = `M ${this._points[0]} ${this._points[1]} L ${this._points[2]} ${this._points[3]}`; + return ( + this.onMouseEnter(e)} + onMouseLeave={e => this.onMouseLeave(e)} + > + + + ); + } +} diff --git a/src/features/cseMachine/java/components/Method.tsx b/src/features/cseMachine/java/components/Method.tsx new file mode 100644 index 0000000000..f871358444 --- /dev/null +++ b/src/features/cseMachine/java/components/Method.tsx @@ -0,0 +1,119 @@ +import { astToString } from 'java-slang/dist/ast/utils/astToString'; +import { Closure as JavaMethod } from 'java-slang/dist/ec-evaluator/types'; +import { KonvaEventObject } from 'konva/lib/Node'; +import React, { RefObject } from 'react'; +import { Circle, Group, Label, Tag, Text } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { IHoverable } from '../../CseMachineTypes'; +import { + getTextHeight, + getTextWidth, + setHoveredCursor, + setUnhoveredCursor, +} from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +export class Method extends Visible implements IHoverable { + private _centerX: number; + + private readonly _tooltipRef: RefObject; + private readonly _tooltip: string; + + constructor( + x: number, + y: number, + private readonly _method: JavaMethod, + ) { + super(); + + // Position. + this._x = x; + this._y = y; + + // Circle. + this._centerX = this._x + Config.FnRadius * 2; + + // Tooltip. + this._tooltipRef = React.createRef(); + this._tooltip = astToString(this._method.mtdOrCon); + } + + get method() { + return this._method; + } + + setX(x: number) { + this._x = x; + this._centerX = this._x + Config.FnRadius * 2; + } + + onMouseEnter = (e: KonvaEventObject) => { + setHoveredCursor(e.currentTarget); + this._tooltipRef.current.show(); + }; + + onMouseLeave = (e: KonvaEventObject) => { + setUnhoveredCursor(e.currentTarget); + this._tooltipRef.current.hide(); + }; + + draw(): React.ReactNode { + return ( + + this.onMouseEnter(e)} + onMouseLeave={e => this.onMouseLeave(e)} + // ref={this.ref} + key={CseMachine.key++} + > + {/* Left outer */} + + {/* Left inner */} + + + + {/* Tooltip */} + + + ); + } +} diff --git a/src/features/cseMachine/java/components/Object.tsx b/src/features/cseMachine/java/components/Object.tsx new file mode 100644 index 0000000000..054a72944a --- /dev/null +++ b/src/features/cseMachine/java/components/Object.tsx @@ -0,0 +1,44 @@ +import { Object as JavaObject } from 'java-slang/dist/ec-evaluator/types'; +import React from 'react'; +import { Group } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { CseMachine } from '../CseMachine'; +import { Frame } from './Frame'; + +export class Object extends Visible { + constructor( + private readonly _frames: Frame[], + private readonly _object: JavaObject, + ) { + super(); + + // Position. + this._x = _frames[0].x(); + this._y = _frames[0].y(); + + // Height and width. + this._height = this._frames.reduce((accHeight, currFrame) => accHeight + currFrame.height(), 0); + this._width = this._frames.reduce((maxWidth, currFrame) => Math.max(maxWidth, currFrame.width()), 0); + } + + get frames() { + return this._frames; + } + + get object() { + return this._object; + } + + getFrame(): Frame { + return this._frames[this._frames.length - 1]; + } + + draw(): React.ReactNode { + return ( + + {this._frames.map(f => f.draw())} + + ); + } +} diff --git a/src/features/cseMachine/java/components/Stash.tsx b/src/features/cseMachine/java/components/Stash.tsx new file mode 100644 index 0000000000..d303dc6fe0 --- /dev/null +++ b/src/features/cseMachine/java/components/Stash.tsx @@ -0,0 +1,90 @@ +import { Stash as JavaStash } from 'java-slang/dist/ec-evaluator/components'; +import { StashItem as JavaStashItem, StructType } from 'java-slang/dist/ec-evaluator/types'; +import React from 'react'; +import { Group } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { CseMachine } from '../CseMachine'; +import { Method } from './Method'; +import { StashItem } from './StashItem'; +import { Variable } from './Variable'; + +export class Stash extends Visible { + private readonly _stashItems: StashItem[] = []; + + constructor(stash: JavaStash) { + super(); + + // Position. + this._x = ControlStashConfig.StashPosX; + this._y = ControlStashConfig.StashPosY; + + // Create each StashItem. + let stashItemX: number = this._x; + for (const stashItem of stash.getStack()) { + const stashItemText = this.getStashItemString(stashItem); + const stashItemStroke = ControlStashConfig.SA_WHITE; + const stashItemReference = this.getStashItemRef(stashItem); + const currStashItem = new StashItem( + stashItemX, + stashItemText, + stashItemStroke, + stashItemReference); + + this._stashItems.push(currStashItem); + stashItemX += currStashItem.width(); + } + + // Height and width. + this._height = ControlStashConfig.StashItemHeight; + this._width = stashItemX - this._x; + } + + draw(): React.ReactNode { + return ( + + {this._stashItems.map(s => s.draw())} + + ); + } + + private getStashItemString = (stashItem: JavaStashItem): string => { + switch (stashItem.kind) { + case "Literal": + return stashItem.literalType.value; + case StructType.VARIABLE: + return "location"; + case StructType.TYPE: + return stashItem.type; + default: + return stashItem.kind.toLowerCase(); + } + } + + private getStashItemRef = (stashItem: JavaStashItem) => { + return stashItem.kind === StructType.CLOSURE + ? CseMachine.environment && + CseMachine.environment.classes + .flatMap(c => c.bindings) + .find(b => b.value instanceof Method && b.value.method === stashItem)?.value as Method + : stashItem.kind === StructType.VARIABLE + ? CseMachine.environment && + (CseMachine.environment.frames + .flatMap(c => c.bindings) + .find(b => b.value instanceof Variable && b.value.variable === stashItem)?.value as Variable || + CseMachine.environment.classes + .flatMap(c => c.bindings) + .find(b => b.value instanceof Variable && b.value.variable === stashItem)?.value as Variable || + CseMachine.environment.objects + .flatMap(o => o.bindings) + .find(b => b.value instanceof Variable && b.value.variable === stashItem)?.value as Variable) + : stashItem.kind === StructType.CLASS + ? CseMachine.environment && + CseMachine.environment.classes.find(c => c.frame === stashItem.frame) + : stashItem.kind === StructType.OBJECT + ? CseMachine.environment && + CseMachine.environment.objects.find(o => o.frame === stashItem.frame) + : undefined; + } +} diff --git a/src/features/cseMachine/java/components/StashItem.tsx b/src/features/cseMachine/java/components/StashItem.tsx new file mode 100644 index 0000000000..2cdb91d8f1 --- /dev/null +++ b/src/features/cseMachine/java/components/StashItem.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { + Group as KonvaGroup, + Label as KonvaLabel, + Tag as KonvaTag, + Text as KonvaText, +} from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { ShapeDefaultProps } from '../../CseMachineConfig'; +import { ControlStashConfig } from '../../CseMachineControlStashConfig'; +import { getTextWidth } from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Frame } from './Frame'; +import { Method } from './Method'; +import { Variable } from './Variable'; + +export class StashItem extends Visible { + private readonly _arrow: Arrow | undefined; + + constructor( + x: number, + private readonly _text: string, + private readonly _stroke: string, + reference?: Method | Frame | Variable, + ) { + super(); + + // Position. + this._x = x; + this._y = ControlStashConfig.StashPosY; + + // Height and width. + this._height = ControlStashConfig.StashItemHeight + ControlStashConfig.StashItemTextPadding * 2; + this._width = ControlStashConfig.StashItemTextPadding * 2 + getTextWidth(this._text); + + // Arrow + if (reference) { + const toY = + reference instanceof Frame + ? reference.y() + reference.name.height() + : reference instanceof Method + ? reference.y() + : reference.y() + reference.type.height(); + this._arrow = new Arrow( + this._x + this._width / 2, + this._y + this._height, + reference.x(), + toY); + } + } + + draw(): React.ReactNode { + const textProps = { + fill: ControlStashConfig.SA_WHITE, + padding: ControlStashConfig.StashItemTextPadding, + fontFamily: ControlStashConfig.FontFamily, + fontSize: ControlStashConfig.FontSize, + fontStyle: ControlStashConfig.FontStyle, + fontVariant: ControlStashConfig.FontVariant, + }; + + const tagProps = { + stroke: this._stroke, + cornerRadius: ControlStashConfig.StashItemCornerRadius, + }; + + return ( + + {/* Text */} + + + + + + {/* Arrow */} + {this._arrow?.draw()} + + ); + } +} diff --git a/src/features/cseMachine/java/components/Text.tsx b/src/features/cseMachine/java/components/Text.tsx new file mode 100644 index 0000000000..89d10c3366 --- /dev/null +++ b/src/features/cseMachine/java/components/Text.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Group as KonvaGroup, Label as KonvaLabel, Text as KonvaText } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { getTextWidth } from '../../CseMachineUtils'; +import { CseMachine } from '../CseMachine'; + +/** this class encapsulates a string to be drawn onto the canvas */ +export class Text extends Visible { + constructor( + private readonly _text: string, + x: number, + y: number, + ) { + super(); + + // Position + this._x = x; + this._y = y; + + // Height and width + this._height = Config.FontSize; + this._width = getTextWidth(this._text); + } + + get text() { + return this._text; + } + + setY(y: number) { + this._y = y; + } + + draw(): React.ReactNode { + const props = { + fontFamily: Config.FontFamily, + fontSize: Config.FontSize, + fontStyle: Config.FontStyle, + fill: Config.SA_WHITE, + }; + + return ( + + + + + + ); + } +} diff --git a/src/features/cseMachine/java/components/Variable.tsx b/src/features/cseMachine/java/components/Variable.tsx new file mode 100644 index 0000000000..a784db2f7e --- /dev/null +++ b/src/features/cseMachine/java/components/Variable.tsx @@ -0,0 +1,109 @@ +import { StructType, Variable as JavaVariable } from 'java-slang/dist/ec-evaluator/types'; +import React from 'react'; +import { Group, Rect } from 'react-konva'; + +import { Visible } from '../../components/Visible'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { CseMachine } from '../CseMachine'; +import { Arrow } from './Arrow'; +import { Text } from './Text'; + +export interface TextOptions { + maxWidth: number; + fontSize: number; + fontFamily: string; + fontStyle: string; + fontVariant: string; + isStringIdentifiable: boolean; +} + +export const defaultOptions: TextOptions = { + maxWidth: Number.MAX_VALUE, // maximum width this text should be + fontFamily: Config.FontFamily.toString(), // default is Arial + fontSize: Number(Config.FontSize), // in pixels. Default is 12 + fontStyle: Config.FontStyle.toString(), // can be normal, bold, or italic. Default is normal + fontVariant: Config.FontVariant.toString(), // can be normal or small-caps. Default is normal + isStringIdentifiable: false // if true, contain strings within double quotation marks "". Default is false +}; + +export class Variable extends Visible { + private readonly _type: Text; + private _value: Text | Arrow; + + constructor( + x: number, + y: number, + private readonly _variable: JavaVariable, + ) { + super(); + + // Position. + this._x = x; + this._y = y; + + // Type. + this._type = new Text( + this._variable.type, + this._x, + this._y); + + // Value. + if (this.variable.value.kind === "Literal") { + this._value = new Text( + this.variable.value.literalType.value, + this._x + Config.TextPaddingX, + this._y + this._type.height() + Config.TextPaddingX); + } else if (this.variable.value.kind === StructType.SYMBOL) { + this._value = new Text( + "", + this._x + Config.TextPaddingX, + this._y + this._type.height() + Config.TextPaddingX); + } else { + this._value = new Text( + "", + this._x + Config.TextPaddingX, + this._y + this._type.height() + Config.TextPaddingX); + } + + // Height and width. + this._height = this._type.height() + this._value.height() + 2 * Config.TextPaddingX; + this._width = Math.max(this._type.width(), this._value.width() + 2 * Config.TextPaddingX); + } + + get variable() { + return this._variable; + } + + set value(value: Arrow) { + this._value = value; + this._height = this._type.height() + Config.FontSize + 2 * Config.TextPaddingX; + this._width = Math.max(this._type.width(), Config.TextMinWidth); + } + + get type() { + return this._type; + } + + draw(): React.ReactNode { + return ( + + {/* Type */} + {this._type.draw()} + + {/* Box */} + + + {/* Text */} + {this._value.draw()} + + ); + } +} From b384d101b7f30fccdf179d6cfa40af91a2c26da3 Mon Sep 17 00:00:00 2001 From: xyliew25 Date: Wed, 10 Apr 2024 22:55:27 +0800 Subject: [PATCH 03/12] Integrate CSEC Visualizer --- src/commons/application/ApplicationTypes.ts | 2 +- src/commons/sagas/PlaygroundSaga.ts | 6 + .../sagas/WorkspaceSaga/helpers/evalCode.ts | 5 +- .../WorkspaceSaga/helpers/updateInspector.ts | 39 ++++-- .../content/SideContentCseMachine.tsx | 123 ++++++++++++------ src/commons/utils/JavaHelper.ts | 53 +++++++- 6 files changed, 177 insertions(+), 51 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index 2015c1007c..0743db122c 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -219,7 +219,7 @@ export const javaLanguages: SALanguage[] = [ variant: Variant.DEFAULT, displayName: 'Java', mainLanguage: SupportedLanguage.JAVA, - supports: {} + supports: { cseMachine: true } } ]; export const cLanguages: SALanguage[] = [ diff --git a/src/commons/sagas/PlaygroundSaga.ts b/src/commons/sagas/PlaygroundSaga.ts index f8e1409145..e7acf4fe01 100644 --- a/src/commons/sagas/PlaygroundSaga.ts +++ b/src/commons/sagas/PlaygroundSaga.ts @@ -5,6 +5,7 @@ import qs from 'query-string'; import { SagaIterator } from 'redux-saga'; import { call, delay, put, race, select } from 'redux-saga/effects'; import CseMachine from 'src/features/cseMachine/CseMachine'; +import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; import { changeQueryString, @@ -100,12 +101,17 @@ export default function* PlaygroundSaga(): SagaIterator { if (newId !== SideContentType.cseMachine) { yield put(toggleUsingCse(false, workspaceLocation)); yield call([CseMachine, CseMachine.clearCse]); + yield call([JavaCseMachine, JavaCseMachine.clearCse]); yield put(updateCurrentStep(-1, workspaceLocation)); yield put(updateStepsTotal(0, workspaceLocation)); yield put(toggleUpdateCse(true, workspaceLocation)); yield put(setEditorHighlightedLines(workspaceLocation, 0, [])); } + if (playgroundSourceChapter === Chapter.FULL_JAVA && newId === SideContentType.cseMachine) { + yield put(toggleUsingCse(true, workspaceLocation)); + } + if ( isSourceLanguage(playgroundSourceChapter) && (newId === SideContentType.substVisualizer || newId === SideContentType.cseMachine) diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts index 6dc00046f8..3f4f6e208c 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts @@ -250,6 +250,9 @@ export function* evalCode( let lastDebuggerResult = yield select( (state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult ); + const isUsingCse = yield select( + (state: OverallState) => state.workspaces["playground"].usingCse + ); // Handles `console.log` statements in fullJS const detachConsole: () => void = @@ -266,7 +269,7 @@ export function* evalCode( : isC ? call(cCompileAndRun, entrypointCode, context) : isJava - ? call(javaRun, entrypointCode, context) + ? call(javaRun, entrypointCode, context, currentStep, isUsingCse) : call( runFilesInContext, isFolderModeEnabled diff --git a/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts b/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts index 7a0305091b..f109aa76e9 100644 --- a/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts +++ b/src/commons/sagas/WorkspaceSaga/helpers/updateInspector.ts @@ -1,24 +1,41 @@ +import { Chapter } from 'js-slang/dist/types'; import { SagaIterator } from 'redux-saga'; import { put, select } from 'redux-saga/effects'; import { OverallState } from '../../../application/ApplicationTypes'; import { actions } from '../../../utils/ActionsHelper'; +import { visualizeJavaCseMachine } from '../../../utils/JavaHelper'; import { visualizeCseMachine } from '../../../utils/JsSlangHelper'; import { WorkspaceLocation } from '../../../workspace/WorkspaceTypes'; export function* updateInspector(workspaceLocation: WorkspaceLocation): SagaIterator { try { - const lastDebuggerResult = yield select( - (state: OverallState) => state.workspaces[workspaceLocation].lastDebuggerResult - ); - const row = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); - // We highlight only one row to show the current line - // If we highlight from start to end, the whole program block will be highlighted at the start - // since the first node is the program node - yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [[row, row]])); - visualizeCseMachine(lastDebuggerResult); + const [lastDebuggerResult, chapter] = yield select((state: OverallState) => [ + state.workspaces[workspaceLocation].lastDebuggerResult, + state.workspaces[workspaceLocation].context.chapter, + ]); + if (chapter === Chapter.FULL_JAVA) { + const controlItem = lastDebuggerResult.context.control.peek(); + let start = -1; + let end = -1; + if (controlItem?.srcNode?.location) { + const node = controlItem.srcNode; + start = node.location.startLine - 1; + end = node.location.endLine ? node.location.endLine - 1 : start; + } + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [[start, end]])); + visualizeJavaCseMachine(lastDebuggerResult); + } else { + const row = lastDebuggerResult.context.runtime.nodes[0].loc.start.line - 1; + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); + // We highlight only one row to show the current line + // If we highlight from start to end, the whole program block will be highlighted at the start + // since the first node is the program node + yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [[row, row]])); + visualizeCseMachine(lastDebuggerResult); + } } catch (e) { // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. yield put(actions.setEditorHighlightedLines(workspaceLocation, 0, [])); diff --git a/src/commons/sideContent/content/SideContentCseMachine.tsx b/src/commons/sideContent/content/SideContentCseMachine.tsx index 009c2aec85..e77c96a220 100644 --- a/src/commons/sideContent/content/SideContentCseMachine.tsx +++ b/src/commons/sideContent/content/SideContentCseMachine.tsx @@ -10,6 +10,7 @@ import { import { IconNames } from '@blueprintjs/icons'; import { Tooltip2 } from '@blueprintjs/popover2'; import classNames from 'classnames'; +import { Chapter } from 'js-slang/dist/types'; import { debounce } from 'lodash'; import React from 'react'; import { HotKeys } from 'react-hotkeys'; @@ -20,6 +21,7 @@ import type { PlaygroundWorkspaceState } from 'src/commons/workspace/WorkspaceTy import CseMachine from 'src/features/cseMachine/CseMachine'; import { CseAnimation } from 'src/features/cseMachine/CseMachineAnimation'; import { Layout } from 'src/features/cseMachine/CseMachineLayout'; +import { CseMachine as JavaCseMachine } from 'src/features/cseMachine/java/CseMachine'; import { InterpreterOutput, OverallState } from '../../application/ApplicationTypes'; import { HighlightedLines } from '../../editor/EditorTypes'; @@ -40,6 +42,7 @@ type State = { width: number; lastStep: boolean; stepLimitExceeded: boolean; + chapter: Chapter; }; type CseMachineProps = OwnProps & StateProps & DispatchProps; @@ -53,6 +56,7 @@ type StateProps = { changepointSteps: number[]; needCseUpdate: boolean; machineOutput: InterpreterOutput[]; + chapter: Chapter; }; type OwnProps = { @@ -85,25 +89,39 @@ class SideContentCseMachineBase extends React.Component width: this.calculateWidth(props.editorWidth), height: this.calculateHeight(props.sideContentHeight), lastStep: false, - stepLimitExceeded: false + stepLimitExceeded: false, + chapter: props.chapter, }; - CseMachine.init( - visualization => { - this.setState({ visualization }, () => CseAnimation.playAnimation()); - if (visualization) this.props.handleAlertSideContent(); - }, - this.state.width, - this.state.height, - (segments: [number, number][]) => { - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - // This comment is copied over from workspace saga - props.setEditorHighlightedLines(0, segments); - }, - // We shouldn't be able to move slider to a step number beyond the step limit - isControlEmpty => { - this.setState({ stepLimitExceeded: false }); - } - ); + if (this.isJava()) { + JavaCseMachine.init( + visualization => this.setState({ visualization }), + (segments: [number, number][]) => { + props.setEditorHighlightedLines(0, segments); + } + ); + } else { + CseMachine.init( + visualization => { + this.setState({ visualization }, () => CseAnimation.playAnimation()); + if (visualization) this.props.handleAlertSideContent(); + }, + this.state.width, + this.state.height, + (segments: [number, number][]) => { + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + // This comment is copied over from workspace saga + props.setEditorHighlightedLines(0, segments); + }, + // We shouldn't be able to move slider to a step number beyond the step limit + isControlEmpty => { + this.setState({ stepLimitExceeded: false }); + } + ); + } + } + + private isJava(): boolean { + return this.props.chapter === Chapter.FULL_JAVA; } private calculateWidth(editorWidth?: string) { @@ -173,7 +191,11 @@ class SideContentCseMachineBase extends React.Component } if (prevProps.needCseUpdate && !this.props.needCseUpdate) { this.stepFirst(); - CseMachine.clearCse(); + if (this.isJava()) { + JavaCseMachine.clearCse(); + } else { + CseMachine.clearCse(); + } } } @@ -210,8 +232,8 @@ class SideContentCseMachineBase extends React.Component onRelease={this.sliderRelease} value={this.state.value < 0 ? 0 : this.state.value} /> -
- +
+ {!this.isJava() && { @@ -248,7 +270,7 @@ class SideContentCseMachineBase extends React.Component /> - + }
{' '} {this.state.visualization && @@ -331,14 +353,32 @@ class SideContentCseMachineBase extends React.Component className={Classes.RUNNING_TEXT} data-testid="cse-machine-default-text" > - The CSE machine generates control, stash and environment model diagrams following a - notation introduced in{' '} - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, - Section 2 - - + {this.isJava() + ? + The CSEC machine generates control, stash, environment and class model diagrams adapted from the + notation introduced in{' '} + + + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, + Section 2 + + {'. '} + + You have chosen the sublanguage{' '} + + Java CSEC + + + : + The CSE machine generates control, stash and environment model diagrams following a + notation introduced in{' '} + + + Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter 3, + Section 2 + + + } .

On this tab, the REPL will be hidden from view, so do check that your code has no @@ -371,13 +411,13 @@ class SideContentCseMachineBase extends React.Component