diff --git a/package.json b/package.json index 29676b4681..805e6d68d2 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "flexboxgrid-helpers": "^1.1.3", "hastscript": "^9.0.0", "java-slang": "^1.0.4", - "js-slang": "^1.0.62", + "js-slang": "^1.0.64", "js-yaml": "^4.1.0", "konva": "^9.2.0", "lodash": "^4.17.21", diff --git a/src/commons/utils/JsSlangHelper.ts b/src/commons/utils/JsSlangHelper.ts index 5591d62240..1dc0826f18 100644 --- a/src/commons/utils/JsSlangHelper.ts +++ b/src/commons/utils/JsSlangHelper.ts @@ -94,7 +94,7 @@ export function visualizeCseMachine({ context }: { context: Context }) { try { CseMachine.drawCse(context); } catch (err) { - throw new Error('CSE machine is not enabled'); + console.error(err); } } diff --git a/src/features/cseMachine/CseMachine.tsx b/src/features/cseMachine/CseMachine.tsx index c3edbd06a4..ce6b61f94a 100644 --- a/src/features/cseMachine/CseMachine.tsx +++ b/src/features/cseMachine/CseMachine.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Layout } from './CseMachineLayout'; import { EnvTree } from './CseMachineTypes'; -import { deepCopyTree, getEnvID } from './CseMachineUtils'; +import { deepCopyTree, getEnvId } from './CseMachineUtils'; type SetVis = (vis: React.ReactNode) => void; type SetEditorHighlightedLines = (segments: [number, number][]) => void; @@ -75,7 +75,7 @@ export default class CseMachine { static drawCse(context: Context) { // store environmentTree at last breakpoint. CseMachine.environmentTree = deepCopyTree(context.runtime.environmentTree as EnvTree); - CseMachine.currentEnvId = getEnvID(context.runtime.environments[0]); + CseMachine.currentEnvId = getEnvId(context.runtime.environments[0]); if (!this.setVis || !context.runtime.control || !context.runtime.stash) throw new Error('CSE machine not initialized'); CseMachine.control = context.runtime.control; diff --git a/src/features/cseMachine/CseMachineConfig.ts b/src/features/cseMachine/CseMachineConfig.ts index 27b275c1fc..761c6c2947 100644 --- a/src/features/cseMachine/CseMachineConfig.ts +++ b/src/features/cseMachine/CseMachineConfig.ts @@ -46,7 +46,9 @@ export const Config = Object.freeze({ MaxExportHeight: 12000, SA_WHITE: '#999999', + SA_FADED_WHITE: '#5b6773', SA_BLUE: '#2c3e50', + SA_FADED_BLUE: '#BBB', PRINT_BACKGROUND: 'white', SA_CURRENT_ITEM: '#030fff', diff --git a/src/features/cseMachine/CseMachineLayout.tsx b/src/features/cseMachine/CseMachineLayout.tsx index 5b2648a7fa..1a3aac41f6 100644 --- a/src/features/cseMachine/CseMachineLayout.tsx +++ b/src/features/cseMachine/CseMachineLayout.tsx @@ -19,23 +19,28 @@ import CseMachine from './CseMachine'; import { CseAnimation } from './CseMachineAnimation'; import { Config, ShapeDefaultProps } from './CseMachineConfig'; import { - Closure, Data, DataArray, EnvTree, EnvTreeNode, GlobalFn, - ReferenceType + NonGlobalFn, + ReferenceType, + StreamFn } from './CseMachineTypes'; import { - convertClosureToGlobalFn, + assert, deepCopyTree, getNextChildren, + isBuiltInFn, isClosure, isDataArray, - isFunction, + isEnvEqual, isGlobalFn, + isNonGlobalFn, isPrimitiveData, + isSourceObject, + isStreamFn, isUnassigned, setDifference } from './CseMachineUtils'; @@ -63,8 +68,6 @@ export class Layout { /** scale factor for zooming and out of canvas */ static scaleFactor = 1.02; - /** the environment tree */ - static environmentTree: EnvTree; /** the global environment */ static globalEnvNode: EnvTreeNode; /** grid of frames */ @@ -79,7 +82,7 @@ export class Layout { static previousStashComponent: StashStack; /** memoized values */ - static values = new Map(); + static values = new Map any), Value>(); /** memoized layout */ static prevLayout: React.ReactNode; static currentDark: React.ReactNode; @@ -140,8 +143,7 @@ export class Layout { Layout.key = 0; // deep copy so we don't mutate the context - Layout.environmentTree = deepCopyTree(envTree); - Layout.globalEnvNode = Layout.environmentTree.root; + Layout.globalEnvNode = deepCopyTree(envTree).root; Layout.control = control; Layout.stash = stash; @@ -201,42 +203,42 @@ export class Layout { const preludeEnvNode = Layout.globalEnvNode.children[0]; const preludeEnv = preludeEnvNode.environment; - const globalEnvNode = Layout.globalEnvNode; - const globalEnv = globalEnvNode.environment; - - const preludeValueKeyMap = new Map( - Object.entries(preludeEnv.head).map(([key, value]) => [value, key]) - ); + const globalEnv = Layout.globalEnvNode.environment; + + // Add bindings from prelude environment head to global environment head + for (const [key, value] of Object.entries(preludeEnv.head)) { + delete preludeEnv.head[key]; + globalEnv.head[key] = value; + if (isStreamFn(value) && isEnvEqual(value.environment, preludeEnv)) { + Object.defineProperty(value, 'environment', { value: globalEnv }); + } + } - // Change environments of each array and closure in the prelude to be the global environment + // Move objects from prelude environment heap to global environment heap for (const value of preludeEnv.heap.getHeap()) { - Object.defineProperty(value, 'environment', { value: globalEnvNode.environment }); - globalEnv.heap.add(value); - const key = preludeValueKeyMap.get(value); - if (key) { - globalEnv.head[key] = value; + Object.defineProperty(value, 'environment', { value: globalEnv }); + if (isDataArray(value)) { + for (const item of value) { + if (isStreamFn(item) && isEnvEqual(item.environment, preludeEnv)) { + Object.defineProperty(item, 'environment', { value: globalEnv }); + } + } } + preludeEnv.heap.move(value, globalEnv.heap); } // update globalEnvNode children - globalEnvNode.resetChildren(preludeEnvNode.children); + Layout.globalEnvNode.resetChildren(preludeEnvNode.children); // update the tail of each child's environment to point to the global environment - globalEnvNode.children.forEach(node => { + Layout.globalEnvNode.children.forEach(node => { node.environment.tail = globalEnv; }); - - // go through new bindings and update closures to be global functions - for (const value of Object.values(globalEnv.head)) { - if (isClosure(value)) { - convertClosureToGlobalFn(value); - } - } } /** remove any global functions not referenced elsewhere in the program */ private static removeUnreferencedGlobalFns(): void { - const referencedGlobalFns = new Set(); + const referencedFns = new Set(); const visitedData = new Set(); const findGlobalFnReferences = (envNode: EnvTreeNode): void => { @@ -244,7 +246,7 @@ export class Layout { const unreferenced = setDifference(envNode.environment.heap.getHeap(), new Set(headValues)); for (const data of headValues) { if (isGlobalFn(data)) { - referencedGlobalFns.add(data); + referencedFns.add(data); } else if (isDataArray(data)) { findGlobalFnReferencesInData(data); } @@ -263,7 +265,7 @@ export class Layout { visitedData.add(data); data.forEach(d => { if (isGlobalFn(d)) { - referencedGlobalFns.add(d); + referencedFns.add(d); } else if (isDataArray(d)) { findGlobalFnReferencesInData(d); } @@ -273,15 +275,18 @@ export class Layout { // First, add any referenced global functions in the stash for (const item of Layout.stash.getStack()) { if (isGlobalFn(item)) { - referencedGlobalFns.add(item); + referencedFns.add(item); } else if (isDataArray(item)) { findGlobalFnReferencesInData(item); } } - // Then, find any references within any arrays inside the global environment heap + // Then, find any references within any arrays inside the global environment heap, + // and also add any non-global functions created in the global frame for (const data of Layout.globalEnvNode.environment.heap.getHeap()) { - if (isDataArray(data)) { + if (isNonGlobalFn(data)) { + referencedFns.add(data); + } else if (isDataArray(data)) { findGlobalFnReferencesInData(data); } } @@ -293,13 +298,12 @@ export class Layout { Object.entries(Layout.globalEnvNode.environment.head).map(([key, value]) => [value, key]) ); + let i = 0; const newHead = {}; const newHeap = new Heap(); - for (const fn of referencedGlobalFns) { - newHead[functionNames.get(fn)!] = fn; - if (fn.hasOwnProperty('environment')) { - newHeap.add(fn as Closure); - } + for (const fn of referencedFns) { + if (isClosure(fn)) newHeap.add(fn); + if (isGlobalFn(fn)) newHead[functionNames.get(fn) ?? `${i++}`] = fn; } // add any arrays from the original heap to the new heap @@ -349,50 +353,44 @@ export class Layout { } } - /** memoize `Value` (used to detect circular references in non-primitive `Value`) */ - static memoizeValue(value: Value): void { - Layout.values.set(value.data, value); - } - - /** create an instance of the corresponding `Value` if it doesn't already exists, - * else, return the existing value */ + /** Creates an instance of the corresponding `Value` if it doesn't already exists, + * else, returns the existing value */ static createValue(data: Data, reference: ReferenceType): Value { if (isUnassigned(data)) { + assert(reference instanceof Binding); return new UnassignedValue(reference); } else if (isPrimitiveData(data)) { return new PrimitiveValue(data, reference); } else { - // try to find if this value is already created - const existingValue = Layout.values.get(data); + const existingValue = Layout.values.get( + isBuiltInFn(data) || isStreamFn(data) ? data : data.id + ); if (existingValue) { existingValue.addReference(reference); return existingValue; } - // else create a new one - let newValue: Value = new PrimitiveValue(null, reference); + let newValue: Value | undefined; if (isDataArray(data)) { newValue = new ArrayValue(data, reference); - } else if (isFunction(data)) { - if (isClosure(data)) { - // normal JS Slang function - newValue = new FnValue(data, reference); - } else { - if (reference instanceof Binding) { - // function from the global env (has no extra props such as env, fnName) - newValue = new GlobalFnValue(data, reference); - } else { - // this should be impossible, since bindings for global function always get - // drawn first, before any other values like arrays get drawn - throw new Error('First reference of global function value is not a binding!'); - } - } + } else if (isGlobalFn(data)) { + assert(reference instanceof Binding); + newValue = new GlobalFnValue(data, reference); + } else if (isNonGlobalFn(data)) { + newValue = new FnValue(data, reference); + } else if (isSourceObject(data)) { + return new PrimitiveValue(data.toReplString(), reference); } - return newValue; + return newValue ?? new PrimitiveValue(null, reference); } } + static memoizeValue(data: GlobalFn | NonGlobalFn | StreamFn | DataArray, value: Value) { + if (isBuiltInFn(data) || isStreamFn(data)) Layout.values.set(data, value); + else Layout.values.set(data.id, value); + } + /** * Scrolls diagram to top left, resets the zoom, and saves the diagram as multiple images of width < MaxExportWidth. */ diff --git a/src/features/cseMachine/CseMachineTypes.ts b/src/features/cseMachine/CseMachineTypes.ts index 33bf239559..eb812dc8a9 100644 --- a/src/features/cseMachine/CseMachineTypes.ts +++ b/src/features/cseMachine/CseMachineTypes.ts @@ -2,7 +2,7 @@ import { EnvTree as EnvironmentTree, EnvTreeNode as EnvironmentTreeNode } from 'js-slang/dist/createContext'; -import JsSlangClosure from 'js-slang/dist/interpreter/closure'; +import JsSlangClosure from 'js-slang/dist/cse-machine/closure'; import { Environment } from 'js-slang/dist/types'; import { KonvaEventObject } from 'konva/lib/Node'; import React from 'react'; @@ -47,12 +47,36 @@ export type Unassigned = symbol; /** types of primitives in JS Slang */ export type Primitive = number | string | boolean | null | undefined; -/** types of in-built functions in JS Slang */ -export type GlobalFn = Function; +/** types of source objects such as runes */ +export type SourceObject = { + [index: string]: any; + toReplString: () => string; +}; -/** types of functions in JS Slang */ +/** types of closures in JS Slang, redefined here for convenience. */ export type Closure = JsSlangClosure; +/** types of built-in functions in JS Slang */ +export type BuiltInFn = () => never; // Use `never` to differentiate from `StreamFn` + +/** types of pre-defined functions in JS Slang */ +export type PredefinedFn = Omit & { predefined: true }; + +/** + * Special type of a function returned from calling `stream`. It is mostly similar to a global + * function, but has the extra `environment` property as it should be drawn next to the frame + * in which `stream` is called. + * + * TODO: remove this and all other `StreamFn` code if `stream` becomes a pre-defined function + */ +export type StreamFn = (() => [any, StreamFn] | null) & { environment: Env }; + +/** types of global functions in JS Slang */ +export type GlobalFn = BuiltInFn | PredefinedFn; + +/** types of global functions in JS Slang */ +export type NonGlobalFn = (Omit & { predefined: false }) | StreamFn; + /** types of arrays in JS Slang */ export type DataArray = Data[] & { readonly id: string; @@ -60,7 +84,7 @@ export type DataArray = Data[] & { }; /** the types of data in the JS Slang context */ -export type Data = Primitive | Closure | GlobalFn | Unassigned | DataArray; +export type Data = Primitive | SourceObject | NonGlobalFn | GlobalFn | Unassigned | DataArray; /** modified `Environment` to store children and associated frame */ export type Env = Environment; diff --git a/src/features/cseMachine/CseMachineUtils.ts b/src/features/cseMachine/CseMachineUtils.ts index 46d8b757ce..1b9ed30437 100644 --- a/src/features/cseMachine/CseMachineUtils.ts +++ b/src/features/cseMachine/CseMachineUtils.ts @@ -1,3 +1,4 @@ +import JsSlangClosure from 'js-slang/dist/cse-machine/closure'; import { AppInstr, ArrLitInstr, @@ -31,6 +32,7 @@ import { Config } from './CseMachineConfig'; import { ControlStashConfig } from './CseMachineControlStashConfig'; import { Layout } from './CseMachineLayout'; import { + BuiltInFn, Closure, Data, DataArray, @@ -39,10 +41,25 @@ import { EnvTree, EnvTreeNode, GlobalFn, + NonGlobalFn, + PredefinedFn, Primitive, - ReferenceType + ReferenceType, + SourceObject, + StreamFn } from './CseMachineTypes'; +class AssertionError extends Error { + constructor(msg?: string) { + super(msg); + this.name = 'AssertionError'; + } +} + +export function assert(condition: boolean, msg?: string): asserts condition { + if (!condition) throw new AssertionError(msg); +} + // TODO: can make use of lodash /** Returns `true` if `x` is an object */ export function isObject(x: any): x is object { @@ -54,6 +71,11 @@ export function isEmptyObject(object: Object): object is EmptyObject { return Object.keys(object).length === 0; } +/** Returns `true` if `x` is a source object, e.g. runes */ +export function isSourceObject(x: any): x is SourceObject { + return isObject(x) && 'toReplString' in x && isFunction(x.toReplString); +} + /** Returns `true` if `object` is `Environment` */ export function isEnvironment(object: Object): object is Environment { return 'head' in object && 'tail' in object && 'name' in object; @@ -93,18 +115,51 @@ export function isFunction(x: any): x is Function { return x && {}.toString.call(x) === '[object Function]'; } +/** Returns `true` if `data` is a built-in function */ +export function isBuiltInFn(data: Data): data is BuiltInFn { + // Extra `environment` check for functions returned from `stream` + // TODO: remove if `stream` becomes a pre-defined function + return isFunction(data) && !isClosure(data) && !{}.hasOwnProperty.call(data, 'environment'); +} + +/** Returns `true` if `data` is a pre-defined function */ +export function isPredefinedFn(data: Data): data is PredefinedFn { + return isClosure(data) && data.predefined; +} + +const closureFields = ['id', 'environment', 'functionName', 'predefined', 'node', 'originalNode']; + /** Returns `true` if `data` is a JS Slang closure */ export function isClosure(data: Data): data is Closure { + const obj = {}; return ( - isFunction(data) && - {}.hasOwnProperty.call(data, 'environment') && - {}.hasOwnProperty.call(data, 'functionName') + data instanceof JsSlangClosure || + (isFunction(data) && + closureFields.reduce((prev, field) => prev && obj.hasOwnProperty.call(data, field), true)) ); } -/** Returns `true` if `x` is a JS Slang function in the global frame */ -export function isGlobalFn(x: any): x is GlobalFn { - return isFunction(x) && !isClosure(x); +/** + * Returns `true` if `data` is a function returned from calling `stream`. + * TODO: remove if `stream` becomes a pre-defined function + */ +export function isStreamFn(data: Data): data is StreamFn { + return isFunction(data) && !isClosure(data) && {}.hasOwnProperty.call(data, 'environment'); +} + +/** Returns `true` if `data` is a function that is built-in or pre-defined */ +export function isGlobalFn(data: Data): data is GlobalFn { + return isBuiltInFn(data) || isPredefinedFn(data); +} + +/** + * Returns `true` if `data` is **not** a function that is built-in or pre-defined. + * In other words, it is either a closure that is not predefined, or a stream function. + * + * TODO: remove checking for `isStreamFn` if `stream` becomes pre-defined + */ +export function isNonGlobalFn(data: Data): data is NonGlobalFn { + return (isClosure(data) && !isPredefinedFn(data)) || isStreamFn(data); } /** Returns `true` if `data` is null */ @@ -160,22 +215,12 @@ export function setDifference(set1: Set, set2: Set) { } } -/** - * Mutates the given closure and converts it into a global function, - * by removing the `functionName` property - */ -export function convertClosureToGlobalFn(fn: Closure) { - delete (fn as Partial).functionName; -} - /** * Returns `true` if `reference` is the main reference of `value`. The main reference priority - * order is as follows: - * 1. The first `ArrayUnit` inside `value.references` that also shares the same environment - * 2. The first `Binding` inside `value.references` that also shares the same environment + * order is the first binding or array unit which shares the same environment with `value`. * * An exception is for a global function value, in which case the global frame binding is - * always prioritised. + * always prioritised over array units. */ export function isMainReference(value: Value, reference: ReferenceType) { if (isGlobalFn(value.data)) { @@ -184,21 +229,25 @@ export function isMainReference(value: Value, reference: ReferenceType) { isEnvEqual(reference.frame.environment, Layout.globalEnvNode.environment) ); } - if (!isClosure(value.data) && !isDataArray(value.data)) { + if (!isNonGlobalFn(value.data) && !isDataArray(value.data)) { return true; } const valueEnv = value.data.environment; - const firstArrayUnit = value.references.find( - r => r instanceof ArrayUnit && isEnvEqual(r.parent.data.environment, valueEnv) + const mainReference = value.references.find(r => + isEnvEqual(r instanceof ArrayUnit ? r.parent.data.environment : r.frame.environment, valueEnv) + ); + return reference === mainReference; +} + +/** + * Returns `true` if `reference` is a dummy reference, + * i.e. it is a dummy binding, or the reference is from an array which is unreferenced + */ +export function isDummyReference(reference: ReferenceType) { + return ( + (reference instanceof Binding && reference.isDummyBinding) || + (reference instanceof ArrayUnit && !reference.parent.isReferenced()) ); - if (firstArrayUnit) { - return reference === firstArrayUnit; - } else { - const firstBinding = value.references.find( - r => r instanceof Binding && isEnvEqual(r.frame.environment, valueEnv) - ); - return reference === firstBinding; - } } /** checks if `value` is a `number` */ @@ -215,6 +264,9 @@ export function isDummyKey(key: string) { return isNumeric(key); } +const canvas = document.createElement('canvas'); +const context = canvas.getContext('2d'); + /** * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. * @@ -225,9 +277,6 @@ export function getTextWidth( text: string, font: string = `${Config.FontStyle} ${Config.FontSize}px ${Config.FontFamily}` ): number { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - if (!context || !text) { return 0; } @@ -261,9 +310,6 @@ export function getTextHeight( font: string = `${Config.FontStyle} ${Config.FontSize}px ${Config.FontFamily}`, fontSize: number = Config.FontSize ): number { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - if (!context || !text) { return 0; } @@ -276,27 +322,32 @@ export function getTextHeight( return numberOfLines * fontSize; } -/** Returns the parameter string of the given function */ -export function getParamsText(data: Function): string { +/** Returns the parameter string of the given function, surrounded by brackets */ +export function getParamsText(data: Closure | GlobalFn | StreamFn): string { if (isClosure(data)) { - return data.node.params.map((node: any) => node.name).join(','); + let params = data.functionName.slice(0, data.functionName.indexOf('=>')).trim(); + if (!params.startsWith('(')) params = '(' + params + ')'; + return params; } else { const fnString = data.toString(); - return fnString.substring(fnString.indexOf('('), fnString.indexOf('{')).trim(); + return fnString.substring(fnString.indexOf('('), fnString.indexOf(')') + 1); } } /** Returns the body string of the given function */ -export function getBodyText(data: Function): string { +export function getBodyText(data: Closure | GlobalFn | StreamFn): string { const fnString = data.toString(); if (isClosure(data)) { let body = - data.node.type === 'FunctionDeclaration' || fnString.substring(0, 8) === 'function' + fnString.substring(0, 8) === 'function' ? fnString.substring(fnString.indexOf('{')) : fnString.substring(fnString.indexOf('=') + 3); if (body[0] !== '{') body = '{\n return ' + body + ';\n}'; return body; + } else if (isStreamFn(data)) { + // TODO: remove if `stream` becomes pre-defined + return '{\n [implementation hidden]\n}'; } else { return fnString.substring(fnString.indexOf('{')); } @@ -659,7 +710,7 @@ export function getControlItemComponent( (accum, level) => accum ? accum - : level.frames.find(frame => frame.environment?.id === getEnvID(envInstr.env)), + : level.frames.find(frame => frame.environment?.id === getEnvId(envInstr.env)), undefined ) ); @@ -751,39 +802,23 @@ export function getControlItemComponent( } export function getStashItemComponent(stashItem: StashValue, stackHeight: number, index: number) { - if (isClosure(stashItem) || isGlobalFn(stashItem) || isDataArray(stashItem)) { - for (const level of Layout.levels) { - for (const frame of level.frames) { - if (isClosure(stashItem) || isGlobalFn(stashItem)) { - const fn: FnValue | GlobalFnValue | undefined = frame.bindings.find(binding => { - if (isClosure(stashItem) && isClosure(binding.data)) { - return binding.data.id === stashItem.id; - } else if (isGlobalFn(stashItem) && isGlobalFn(binding.data)) { - return binding.data?.toString() === stashItem.toString(); - } - return false; - })?.value as unknown as FnValue | GlobalFnValue; - if (fn) return new StashItemComponent(stashItem, stackHeight, index, fn); - } else { - const ar: ArrayValue | undefined = frame.bindings.find(binding => { - if (isDataArray(binding.data)) { - return binding.data === stashItem; - } - return false; - })?.value as ArrayValue; - if (ar) return new StashItemComponent(stashItem, stackHeight, index, ar); - } - } + let arrowTo: ArrayValue | FnValue | GlobalFnValue | undefined; + if (isFunction(stashItem) || isDataArray(stashItem)) { + if (isClosure(stashItem) || isDataArray(stashItem)) { + arrowTo = Layout.values.get(stashItem.id) as ArrayValue | FnValue; + } else { + arrowTo = Layout.values.get(stashItem) as FnValue | GlobalFnValue; } } - return new StashItemComponent(stashItem, stackHeight, index); + return new StashItemComponent(stashItem, stackHeight, index, arrowTo); } // Helper function to get environment ID. Accounts for the hidden prelude environment right // after the global environment. Does not need to be used for frame environments, only for // environments from the context. -export const getEnvID = (environment: Environment): string => - environment.tail?.name === 'global' ? environment.tail.id : environment.id; +export const getEnvId = (environment: Environment): string => { + return environment.name === 'prelude' ? environment.tail!.id : environment.id; +}; // Function that returns whether the stash item will be popped off in the next step export const isStashItemInDanger = (stashIndex: number): boolean => { @@ -819,6 +854,9 @@ export const isStashItemInDanger = (stashIndex: number): boolean => { export const defaultSAColor = () => CseMachine.getPrintableMode() ? Config.SA_BLUE : Config.SA_WHITE; +export const fadedSAColor = () => + CseMachine.getPrintableMode() ? Config.SA_FADED_BLUE : Config.SA_FADED_WHITE; + export const stackItemSAColor = (index: number) => isStashItemInDanger(index) ? ControlStashConfig.STASH_DANGER_ITEM diff --git a/src/features/cseMachine/__tests__/CseMachine.tsx b/src/features/cseMachine/__tests__/CseMachine.tsx index e10468814f..ac2827a432 100644 --- a/src/features/cseMachine/__tests__/CseMachine.tsx +++ b/src/features/cseMachine/__tests__/CseMachine.tsx @@ -192,7 +192,7 @@ const codeSamplesControlStash = [ { const math_sin = x => x; } - math_sin(math_PI / 2); + math_sin(math_PI / 2); `, 5 ], diff --git a/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap b/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap index d96a33d9bd..9e49488aff 100644 --- a/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap +++ b/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap @@ -1979,7 +1979,7 @@ exports[`CSE Machine Control Stash correctly renders: global environments are tr onMouseLeave={[Function]} > { return typeof val === 'string' ? `'${val}'`.trim() - : isClosure(val) + : isNonGlobalFn(val) ? 'closure' : isDataArray(val) ? arrowTo ? 'pair/array' : JSON.stringify(val) + : isSourceObject(val) + ? val.toReplString() : String(value); }; this.text = truncateText( @@ -69,6 +72,7 @@ export class StashItemComponent extends Visible implements IHoverable { this._x = ControlStashConfig.StashPosX + stackWidth; this._y = ControlStashConfig.StashPosY; if (arrowTo) { + arrowTo.markAsReferenced(); this.arrow = new ArrowFromStashItemComponent(this).to(arrowTo) as ArrowFromStashItemComponent; } } diff --git a/src/features/cseMachine/components/Text.tsx b/src/features/cseMachine/components/Text.tsx index de21ab3073..cfbaea80b6 100644 --- a/src/features/cseMachine/components/Text.tsx +++ b/src/features/cseMachine/components/Text.tsx @@ -5,7 +5,13 @@ import { Label as KonvaLabel, Tag as KonvaTag, Text as KonvaText } from 'react-k import { Config, ShapeDefaultProps } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; import { Data, IHoverable } from '../CseMachineTypes'; -import { getTextWidth, setHoveredCursor, setUnhoveredCursor } from '../CseMachineUtils'; +import { + defaultSAColor, + fadedSAColor, + getTextWidth, + setHoveredCursor, + setUnhoveredCursor +} from '../CseMachineUtils'; import { Visible } from './Visible'; export interface TextOptions { @@ -15,6 +21,7 @@ export interface TextOptions { fontStyle: string; fontVariant: string; isStringIdentifiable: boolean; + faded: boolean; } export const defaultOptions: TextOptions = { @@ -23,7 +30,8 @@ export const defaultOptions: TextOptions = { fontSize: Config.FontSize, // in pixels. Default is 12 fontStyle: Config.FontStyle, // can be normal, bold, or italic. Default is normal fontVariant: Config.FontVariant, // can be normal or small-caps. Default is normal - isStringIdentifiable: false // if true, contain strings within double quotation marks "". Default is false + isStringIdentifiable: false, // if true, contain strings within double quotation marks "". Default is false + faded: false // if true, draws text with a lighter shade }; /** this class encapsulates a string to be drawn onto the canvas */ @@ -84,7 +92,7 @@ export class Text extends Visible implements IHoverable { fontFamily: this.options.fontFamily, fontSize: this.options.fontSize, fontStyle: this.options.fontStyle, - fill: Config.SA_WHITE + fill: this.options.faded ? fadedSAColor() : defaultSAColor() }; return ( diff --git a/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx b/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx index f4031acd31..b670f06e4a 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx @@ -9,6 +9,11 @@ import { GenericArrow } from './GenericArrow'; /** this class encapsulates an GenericArrow to be drawn between 2 points */ export class ArrowFromArrayUnit extends GenericArrow { + constructor(from: ArrayUnit) { + super(from); + this.faded = !from.parent.isReferenced(); + } + protected calculateSteps() { const from = this.source; const to = this.target; diff --git a/src/features/cseMachine/components/arrows/ArrowFromFn.tsx b/src/features/cseMachine/components/arrows/ArrowFromFn.tsx index 79b2cb0ad6..b5eb1cbf1b 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromFn.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromFn.tsx @@ -7,6 +7,11 @@ import { GenericArrow } from './GenericArrow'; /** this class encapsulates an GenericArrow to be drawn between 2 points */ export class ArrowFromFn extends GenericArrow { + constructor(from: FnValue | GlobalFnValue) { + super(from); + this.faded = !from.isReferenced(); + } + protected calculateSteps() { const from = this.source; const to = this.target; diff --git a/src/features/cseMachine/components/arrows/GenericArrow.tsx b/src/features/cseMachine/components/arrows/GenericArrow.tsx index 180b891792..fe91a33530 100644 --- a/src/features/cseMachine/components/arrows/GenericArrow.tsx +++ b/src/features/cseMachine/components/arrows/GenericArrow.tsx @@ -3,7 +3,7 @@ import { Arrow as KonvaArrow, Group as KonvaGroup, Path as KonvaPath } from 'rea import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; import { IVisible, StepsArray } from '../../CseMachineTypes'; -import { defaultSAColor } from '../../CseMachineUtils'; +import { defaultSAColor, fadedSAColor } from '../../CseMachineUtils'; import { Visible } from '../Visible'; /** this class encapsulates an arrow to be drawn between 2 points */ @@ -12,6 +12,7 @@ export class GenericArrow exte points: number[] = []; source: Source; target: Target | undefined; + faded: boolean = false; constructor(from: Source) { super(); @@ -20,6 +21,7 @@ export class GenericArrow exte this._x = from.x(); this._y = from.y(); } + path(): string { return this._path; } @@ -96,6 +98,7 @@ export class GenericArrow exte } // end path this._path += `L ${points[points.length - 2]} ${points[points.length - 1]} `; + const stroke = this.faded ? fadedSAColor() : defaultSAColor(); return ( exte > exte = React.createRef(); constructor( /** underlying JS Slang function (contains extra props) */ - readonly data: Closure, + readonly data: NonGlobalFn, /** what this value is being referenced by */ firstReference: ReferenceType ) { super(); - // Workaround for `stream_tail`, as the closure will always be linked to the - // "functionBodyEnvironment" which might be empty - if (isEmptyEnvironment(data.environment)) { - data.environment = getNonEmptyEnv(data.environment); - } - Layout.memoizeValue(this); + Layout.memoizeValue(data, this); this.addReference(firstReference); } handleNewReference(newReference: ReferenceType): void { if (!isMainReference(this, newReference)) return; + // derive the coordinates from the main reference (binding / array unit) if (newReference instanceof Binding) { - this._x = newReference.frame.x() + newReference.frame.width() + Config.FrameMarginX / 4; + this._x = newReference.frame.x() + newReference.frame.width() + Config.FrameMarginX; this._y = newReference.y(); this.centerX = this._x + this.radius * 2; } else { @@ -86,12 +83,10 @@ export class FnValue extends Value implements IHoverable { this._width = this.radius * 4; this._height = this.radius * 2; - this.enclosingEnvNode = Layout.environmentTree.getTreeNode( - this.data.environment - ) as EnvTreeNode; - this.fnName = this.data.functionName; + this.enclosingFrame = Frame.getFrom(this.data.environment); + this.fnName = isStreamFn(this.data) ? '' : this.data.functionName; - this.paramsText = `params: (${getParamsText(this.data)})`; + this.paramsText = `params: ${getParamsText(this.data)}`; this.bodyText = `body: ${getBodyText(this.data)}`; this.exportBodyText = (this.bodyText.length > 23 ? this.bodyText.slice(0, 20) : this.bodyText) @@ -122,9 +117,10 @@ export class FnValue extends Value implements IHoverable { if (this.fnName === undefined) { throw new Error('Error: Closure has no main reference and is not initialised!'); } - this._arrow = - this.enclosingEnvNode.frame && - (new ArrowFromFn(this).to(this.enclosingEnvNode.frame) as ArrowFromFn); + if (this.enclosingFrame) { + this._arrow = new ArrowFromFn(this).to(this.enclosingFrame) as ArrowFromFn; + } + const stroke = this.isReferenced() ? defaultSAColor() : fadedSAColor(); return ( {CseMachine.getPrintableMode() ? ( diff --git a/src/features/cseMachine/components/values/GlobalFnValue.tsx b/src/features/cseMachine/components/values/GlobalFnValue.tsx index 1f88b826ae..5bd18587b0 100644 --- a/src/features/cseMachine/components/values/GlobalFnValue.tsx +++ b/src/features/cseMachine/components/values/GlobalFnValue.tsx @@ -12,7 +12,13 @@ import CseMachine from '../../CseMachine'; import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; import { GlobalFn, IHoverable } from '../../CseMachineTypes'; -import { defaultSAColor, getBodyText, getParamsText, getTextWidth } from '../../CseMachineUtils'; +import { + defaultSAColor, + fadedSAColor, + getBodyText, + getParamsText, + getTextWidth +} from '../../CseMachineUtils'; import { ArrowFromFn } from '../arrows/ArrowFromFn'; import { Binding } from '../Binding'; import { Value } from './Value'; @@ -43,11 +49,10 @@ export class GlobalFnValue extends Value implements IHoverable { mainReference: Binding ) { super(); - Layout.memoizeValue(this); - this.references = [mainReference]; + Layout.memoizeValue(data, this); // derive the coordinates from the main reference (binding) - this._x = mainReference.frame.x() + mainReference.frame.width() + Config.FrameMarginX / 4; + this._x = mainReference.frame.x() + mainReference.frame.width() + Config.FrameMarginX; this._y = mainReference.y(); this.centerX = this._x + this.radius * 2; this._y += this.radius; @@ -64,19 +69,17 @@ export class GlobalFnValue extends Value implements IHoverable { .join('\n') + ' ...'; this.tooltip = `${this.paramsText}\n${this.bodyText}`; this.exportTooltip = `${this.paramsText}\n${this.exportBodyText}`; - this.tooltipWidth = - Math.max(getTextWidth(this.paramsText), getTextWidth(this.bodyText)) + Config.TextPaddingX; + this.tooltipWidth = Math.max(getTextWidth(this.paramsText), getTextWidth(this.bodyText)); this.exportTooltipWidth = Math.max( getTextWidth(this.paramsText), getTextWidth(this.exportBodyText) ); - } - handleNewReference(): void { - // do nothing, since the first reference which is a binding in the global frame, - // is also the main reference + this.addReference(mainReference); } + handleNewReference(): void {} + arrow(): ArrowFromFn | undefined { return this._arrow; } @@ -108,15 +111,15 @@ export class GlobalFnValue extends Value implements IHoverable { draw(): React.ReactNode { this._isDrawn = true; - this._arrow = - Layout.globalEnvNode.frame && - (new ArrowFromFn(this).to(Layout.globalEnvNode.frame) as ArrowFromFn); + if (Layout.globalEnvNode.frame) { + this._arrow = new ArrowFromFn(this).to(Layout.globalEnvNode.frame) as ArrowFromFn; + } + const stroke = this.isReferenced() ? defaultSAColor() : fadedSAColor(); return ( this.onMouseEnter(e)} onMouseLeave={e => this.onMouseLeave(e)} - onClick={e => this.onClick(e)} ref={this.ref} > {CseMachine.getPrintableMode() ? ( @@ -187,7 +190,7 @@ export class GlobalFnValue extends Value implements IHoverable { /> )} - {Layout.globalEnvNode.frame && new ArrowFromFn(this).to(Layout.globalEnvNode.frame).draw()} + {this._arrow?.draw()} ); } diff --git a/src/features/cseMachine/components/values/PrimitiveValue.tsx b/src/features/cseMachine/components/values/PrimitiveValue.tsx index 837f6f4773..836f0348de 100644 --- a/src/features/cseMachine/components/values/PrimitiveValue.tsx +++ b/src/features/cseMachine/components/values/PrimitiveValue.tsx @@ -21,7 +21,6 @@ export class PrimitiveValue extends Value { reference: ReferenceType ) { super(); - this.references = [reference]; // derive the coordinates from the main reference (binding / array unit) if (reference instanceof Binding) { @@ -30,23 +29,32 @@ export class PrimitiveValue extends Value { this.text = new Text(this.data, this.x(), this.y(), { isStringIdentifiable: true }); } else { const maxWidth = reference.width(); - const textWidth = Math.min(getTextWidth(String(this.data)), maxWidth); + const textWidth = isNull(this.data) ? 0 : Math.min(getTextWidth(String(this.data)), maxWidth); this._x = reference.x() + (reference.width() - textWidth) / 2; this._y = reference.y() + (reference.height() - Config.FontSize) / 2; this.text = isNull(this.data) ? new ArrayNullUnit(reference) : new Text(this.data, this.x(), this.y(), { maxWidth: maxWidth, - isStringIdentifiable: true + isStringIdentifiable: true, + faded: true }); } this._width = this.text.width(); this._height = this.text.height(); + this.addReference(reference); } handleNewReference(): void { - throw new Error('Primitive values cannot have more than one reference!'); + if (this.references.length > 1) + throw new Error('Primitive values cannot have more than one reference!'); + } + + markAsReferenced() { + if (this.isReferenced()) return; + super.markAsReferenced(); + if (this.text instanceof Text) this.text.options.faded = false; } draw(): React.ReactNode { diff --git a/src/features/cseMachine/components/values/UnassignedValue.tsx b/src/features/cseMachine/components/values/UnassignedValue.tsx index 84e3f77b19..89a796b6d2 100644 --- a/src/features/cseMachine/components/values/UnassignedValue.tsx +++ b/src/features/cseMachine/components/values/UnassignedValue.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Config } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; -import { ReferenceType, Unassigned } from '../../CseMachineTypes'; +import { Unassigned } from '../../CseMachineTypes'; import { getTextWidth } from '../../CseMachineUtils'; import { Binding } from '../Binding'; import { Text } from '../Text'; @@ -13,27 +13,15 @@ export class UnassignedValue extends Value { readonly data: Unassigned = Symbol(); readonly text: Text; - constructor(reference: ReferenceType) { + constructor(reference: Binding) { super(); this.references = [reference]; - // derive the coordinates from the main reference (binding / array unit) - if (reference instanceof Binding) { - this._x = reference.x() + getTextWidth(reference.keyString) + Config.TextPaddingX; - this._y = reference.y(); - this.text = new Text(Config.UnassignedData, this._x, this._y, { - isStringIdentifiable: false - }); - } else { - const maxWidth = reference.width(); - const textWidth = Math.min(getTextWidth(String(this.data)), maxWidth); - this._x = reference.x() + (reference.width() - textWidth) / 2; - this._y = reference.y() + (reference.height() - Config.FontSize) / 2; - this.text = new Text(Config.UnassignedData, this._x, this._y, { - maxWidth: maxWidth, - isStringIdentifiable: false - }); - } + this._x = reference.x() + getTextWidth(reference.keyString) + Config.TextPaddingX; + this._y = reference.y(); + this.text = new Text(Config.UnassignedData, this._x, this._y, { + isStringIdentifiable: false + }); this._width = this.text.width(); this._height = this.text.height(); diff --git a/src/features/cseMachine/components/values/Value.tsx b/src/features/cseMachine/components/values/Value.tsx index d713793ebb..58262bcd8f 100644 --- a/src/features/cseMachine/components/values/Value.tsx +++ b/src/features/cseMachine/components/values/Value.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Data, ReferenceType } from '../../CseMachineTypes'; +import { isDummyReference } from '../../CseMachineUtils'; import { Visible } from '../Visible'; /** the value of a `Binding` or an `ArrayUnit` */ @@ -8,6 +9,20 @@ export abstract class Value extends Visible { /** the underlying data of this value */ abstract readonly data: Data; + /** + * if the value has actual references, i.e. the references + * are not from dummy bindings or from unreferenced arrays + */ + private _isReferenced: boolean = false; + + isReferenced() { + return this._isReferenced; + } + + markAsReferenced() { + this._isReferenced = true; + } + /** references to this value */ public references: ReferenceType[] = []; @@ -15,6 +30,9 @@ export abstract class Value extends Visible { addReference(newReference: ReferenceType): void { this.references.push(newReference); this.handleNewReference(newReference); + if (!this.isReferenced() && !isDummyReference(newReference)) { + this.markAsReferenced(); + } } /** additional logic to handle new references */ diff --git a/yarn.lock b/yarn.lock index 914a2c5dcd..5058de0657 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8638,10 +8638,10 @@ js-sdsl@4.3.0, js-sdsl@^4.1.4: resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== -js-slang@^1.0.62: - version "1.0.62" - resolved "https://registry.yarnpkg.com/js-slang/-/js-slang-1.0.62.tgz#516352f5db0738d1bca91b1146e3a7e012355127" - integrity sha512-rngDsPDpEsx2VFmJfHxbA9O2AYIR8czwyJgDUC0EeJCBYOVJf8TJLWJEN/85795x5xTrhaZ3qc/QyP84qv53Sw== +js-slang@^1.0.64: + version "1.0.64" + resolved "https://registry.yarnpkg.com/js-slang/-/js-slang-1.0.64.tgz#978ffdc9146778e4a417367dea0758187609db1c" + integrity sha512-mKaFhbK1pTWbuZsvwMoB6qgq0lUbd3MjYOwB+xD13B3zXJjGPNwvN/xycPSvpAgtb7Qv2KtPJu1E4mmhAL1wwA== dependencies: "@babel/parser" "^7.19.4" "@joeychenofficial/alt-ergo-modified" "^2.4.0"