From 0b9492c23338a2e6b90f3261103dbe52c1fe3b4c Mon Sep 17 00:00:00 2001 From: CZX Date: Sun, 7 Apr 2024 08:52:48 +0800 Subject: [PATCH 1/9] Initial Commit --- src/commons/utils/JsSlangHelper.ts | 1 + src/features/cseMachine/CseMachineConfig.ts | 2 + src/features/cseMachine/CseMachineLayout.tsx | 80 ++++++++----------- src/features/cseMachine/CseMachineTypes.ts | 23 +++++- src/features/cseMachine/CseMachineUtils.ts | 59 ++++++++++---- .../cseMachine/components/ArrayEmptyUnit.tsx | 26 +----- .../cseMachine/components/ArrayNullUnit.tsx | 6 +- .../cseMachine/components/ArrayUnit.tsx | 8 +- src/features/cseMachine/components/Frame.tsx | 9 ++- .../components/values/ArrayValue.tsx | 4 +- .../cseMachine/components/values/FnValue.tsx | 41 +++++----- .../components/values/GlobalFnValue.tsx | 25 ++++-- .../components/values/UnassignedValue.tsx | 26 ++---- 13 files changed, 160 insertions(+), 150 deletions(-) diff --git a/src/commons/utils/JsSlangHelper.ts b/src/commons/utils/JsSlangHelper.ts index 5591d62240..37d6f0b23d 100644 --- a/src/commons/utils/JsSlangHelper.ts +++ b/src/commons/utils/JsSlangHelper.ts @@ -94,6 +94,7 @@ export function visualizeCseMachine({ context }: { context: Context }) { try { CseMachine.drawCse(context); } catch (err) { + console.error(err); throw new Error('CSE machine is not enabled'); } } diff --git a/src/features/cseMachine/CseMachineConfig.ts b/src/features/cseMachine/CseMachineConfig.ts index 27b275c1fc..db925db1b9 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: '#4d5c6b', SA_BLUE: '#2c3e50', + SA_FADED_BLUE: '#6e8faf', PRINT_BACKGROUND: 'white', SA_CURRENT_ITEM: '#030fff', diff --git a/src/features/cseMachine/CseMachineLayout.tsx b/src/features/cseMachine/CseMachineLayout.tsx index 5b2648a7fa..0e212e7140 100644 --- a/src/features/cseMachine/CseMachineLayout.tsx +++ b/src/features/cseMachine/CseMachineLayout.tsx @@ -28,14 +28,14 @@ import { ReferenceType } from './CseMachineTypes'; import { - convertClosureToGlobalFn, + assert, deepCopyTree, getNextChildren, isClosure, isDataArray, - isFunction, isGlobalFn, isPrimitiveData, + isStreamFn, isUnassigned, setDifference } from './CseMachineUtils'; @@ -63,8 +63,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 +77,7 @@ export class Layout { static previousStashComponent: StashStack; /** memoized values */ - static values = new Map(); + static values = new Map(); /** memoized layout */ static prevLayout: React.ReactNode; static currentDark: React.ReactNode; @@ -140,8 +138,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; @@ -211,9 +208,11 @@ export class Layout { // Change environments of each array and closure in the prelude to be the global environment for (const value of preludeEnv.heap.getHeap()) { Object.defineProperty(value, 'environment', { value: globalEnvNode.environment }); - globalEnv.heap.add(value); + preludeEnv.heap.move(value, globalEnv.heap); + // globalEnv.heap.add(value); const key = preludeValueKeyMap.get(value); if (key) { + delete preludeEnv.head[key]; globalEnv.head[key] = value; } } @@ -225,18 +224,11 @@ export class 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 +236,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 +255,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 +265,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 closures created in the global frame for (const data of Layout.globalEnvNode.environment.heap.getHeap()) { - if (isDataArray(data)) { + if (isClosure(data)) { + referencedFns.add(data); + } else if (isDataArray(data)) { findGlobalFnReferencesInData(data); } } @@ -295,10 +290,14 @@ export class Layout { 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 (isGlobalFn(fn)) { + newHead[functionNames.get(fn)!] = fn; + if (fn.hasOwnProperty('environment')) { + newHeap.add(fn as Closure); + } + } else { + newHeap.add(fn); } } @@ -349,46 +348,31 @@ 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); if (existingValue) { existingValue.addReference(reference); return existingValue; } - // else create a new one let newValue: Value = new PrimitiveValue(null, reference); 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) || isStreamFn(data)) { + newValue = new GlobalFnValue(data, reference); + } else { + newValue = new FnValue(data, reference); } + Layout.values.set(data, newValue); return newValue; } } diff --git a/src/features/cseMachine/CseMachineTypes.ts b/src/features/cseMachine/CseMachineTypes.ts index 33bf239559..f638285d76 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,10 +47,25 @@ 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 built-in functions in JS Slang */ +export type BuiltInFn = Exclude; -/** types of functions in JS Slang */ +/** types of pre-defined functions in JS Slang */ +export type PredefinedFn = Omit & { predefined: true }; + +/** types of global functions in JS Slang */ +export type GlobalFn = BuiltInFn | PredefinedFn; + +/** + * 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 = BuiltInFn & { environment: Env }; + +/** types of closures in JS Slang, redefined here for convenience. */ export type Closure = JsSlangClosure; /** types of arrays in JS Slang */ diff --git a/src/features/cseMachine/CseMachineUtils.ts b/src/features/cseMachine/CseMachineUtils.ts index 46d8b757ce..e459eae591 100644 --- a/src/features/cseMachine/CseMachineUtils.ts +++ b/src/features/cseMachine/CseMachineUtils.ts @@ -31,6 +31,7 @@ import { Config } from './CseMachineConfig'; import { ControlStashConfig } from './CseMachineControlStashConfig'; import { Layout } from './CseMachineLayout'; import { + BuiltInFn, Closure, Data, DataArray, @@ -39,10 +40,23 @@ import { EnvTree, EnvTreeNode, GlobalFn, + PredefinedFn, Primitive, - ReferenceType + ReferenceType, + 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 { @@ -93,20 +107,36 @@ export function isFunction(x: any): x is Function { return x && {}.toString.call(x) === '[object Function]'; } +/** Returns `true` if `data` is a built-in function */ +function isBuiltInFn(data: Data): data is BuiltInFn { + return isFunction(data) && !isClosure(data); +} + +/** Returns `true` if `data` is a pre-defined function */ +function isPredefinedFn(data: Data): data is PredefinedFn { + return isClosure(data) && data.predefined; +} + +/** Returns `true` if `data` is a function in the global frame */ +export function isGlobalFn(data: Data): data is GlobalFn { + return isBuiltInFn(data) || isPredefinedFn(data); +} + +/** Returns `true` if `data` is a function returned from calling `stream` */ +export function isStreamFn(data: Data): data is StreamFn { + return isBuiltInFn(data) && {}.hasOwnProperty.call(data, 'environment'); +} + /** Returns `true` if `data` is a JS Slang closure */ export function isClosure(data: Data): data is Closure { return ( isFunction(data) && {}.hasOwnProperty.call(data, 'environment') && - {}.hasOwnProperty.call(data, 'functionName') + {}.hasOwnProperty.call(data, 'functionName') && + {}.hasOwnProperty.call(data, 'predefined') ); } -/** 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 null */ export function isNull(data: Data): data is null { return data === null; @@ -160,14 +190,6 @@ 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: @@ -277,7 +299,7 @@ export function getTextHeight( } /** Returns the parameter string of the given function */ -export function getParamsText(data: Function): string { +export function getParamsText(data: GlobalFn | Closure): string { if (isClosure(data)) { return data.node.params.map((node: any) => node.name).join(','); } else { @@ -291,7 +313,7 @@ export function getBodyText(data: Function): string { const fnString = data.toString(); if (isClosure(data)) { let body = - data.node.type === 'FunctionDeclaration' || fnString.substring(0, 8) === 'function' + data.node.type === 'FunctionExpression' || fnString.substring(0, 8) === 'function' ? fnString.substring(fnString.indexOf('{')) : fnString.substring(fnString.indexOf('=') + 3); @@ -819,6 +841,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/components/ArrayEmptyUnit.tsx b/src/features/cseMachine/components/ArrayEmptyUnit.tsx index 47ce7e2521..b65c82e6bb 100644 --- a/src/features/cseMachine/components/ArrayEmptyUnit.tsx +++ b/src/features/cseMachine/components/ArrayEmptyUnit.tsx @@ -3,42 +3,22 @@ import { Rect } from 'react-konva'; import { ShapeDefaultProps } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; -import { DataArray } from '../CseMachineTypes'; -import { defaultSAColor } from '../CseMachineUtils'; +import { defaultSAColor, fadedSAColor } from '../CseMachineUtils'; import { ArrayValue } from './values/ArrayValue'; import { Visible } from './Visible'; /** this classes encapsulates an empty array */ export class ArrayEmptyUnit extends Visible { readonly value: null = null; - - readonly data: DataArray; + readonly data = undefined; constructor(readonly parent: ArrayValue) { super(); - this.data = parent.data; this._x = this.parent.x(); this._y = this.parent.y(); this._height = this.parent.height(); this._width = this.parent.width(); } - x(): number { - return this._x; - } - y(): number { - return this._y; - } - height(): number { - return this._height; - } - width(): number { - return this._width; - } - updatePosition = () => { - this._x = this.parent.x(); - this._y = this.parent.y(); - }; - reset(): void {} draw(): React.ReactNode { return ( @@ -49,7 +29,7 @@ export class ArrayEmptyUnit extends Visible { y={this.y()} width={this.width()} height={this.height()} - stroke={defaultSAColor()} + stroke={this.parent.unreferenced ? fadedSAColor() : defaultSAColor()} ref={this.ref} /> ); diff --git a/src/features/cseMachine/components/ArrayNullUnit.tsx b/src/features/cseMachine/components/ArrayNullUnit.tsx index f95a6c27b8..73bd1a37c3 100644 --- a/src/features/cseMachine/components/ArrayNullUnit.tsx +++ b/src/features/cseMachine/components/ArrayNullUnit.tsx @@ -3,13 +3,13 @@ import { Line as KonvaLine } from 'react-konva'; import { Config, ShapeDefaultProps } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; -import { defaultSAColor } from '../CseMachineUtils'; +import { defaultSAColor, fadedSAColor } from '../CseMachineUtils'; import { ArrayUnit } from './ArrayUnit'; import { Visible } from './Visible'; /** this classes encapsulates a null value in Source pairs or arrays */ export class ArrayNullUnit extends Visible { - constructor(reference: ArrayUnit) { + constructor(readonly reference: ArrayUnit) { super(); this._x = reference.x(); this._y = reference.y(); @@ -23,7 +23,7 @@ export class ArrayNullUnit extends Visible { {...ShapeDefaultProps} key={Layout.key++} points={[this.x(), this.y() + this.height(), this.x() + this.width(), this.y()]} - stroke={defaultSAColor()} + stroke={this.reference.parent.unreferenced ? fadedSAColor() : defaultSAColor()} hitStrokeWidth={Config.DataHitStrokeWidth} ref={this.ref} listening={false} diff --git a/src/features/cseMachine/components/ArrayUnit.tsx b/src/features/cseMachine/components/ArrayUnit.tsx index ee483fdaf8..93c2763e7d 100644 --- a/src/features/cseMachine/components/ArrayUnit.tsx +++ b/src/features/cseMachine/components/ArrayUnit.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Config } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; import { Data } from '../CseMachineTypes'; -import { defaultSAColor } from '../CseMachineUtils'; +import { defaultSAColor, fadedSAColor } from '../CseMachineUtils'; import { Arrow } from './arrows/Arrow'; import { ArrowFromArrayUnit } from './arrows/ArrowFromArrayUnit'; import { RoundedRect } from './shapes/RoundedRect'; @@ -24,7 +24,6 @@ export class ArrayUnit extends Visible { readonly isLastUnit: boolean; /** check if this unit is the main reference of the value */ readonly isMainReference: boolean; - parent: ArrayValue; arrow: Arrow | undefined = undefined; index: Text; @@ -34,10 +33,9 @@ export class ArrayUnit extends Visible { /** the value this unit contains*/ readonly data: Data, /** parent of this unit */ - parent: ArrayValue + readonly parent: ArrayValue ) { super(); - this.parent = parent; this._x = this.parent.x() + this.idx * Config.DataUnitWidth; this._y = this.parent.y(); this._height = Config.DataUnitHeight; @@ -78,7 +76,7 @@ export class ArrayUnit extends Visible { y={this.y()} width={this.width()} height={this.height()} - stroke={defaultSAColor()} + stroke={this.parent.unreferenced ? fadedSAColor() : defaultSAColor()} hitStrokeWidth={Config.DataHitStrokeWidth} fillEnabled={false} onMouseEnter={this.onMouseEnter} diff --git a/src/features/cseMachine/components/Frame.tsx b/src/features/cseMachine/components/Frame.tsx index c3ac074fee..fc6523d912 100644 --- a/src/features/cseMachine/components/Frame.tsx +++ b/src/features/cseMachine/components/Frame.tsx @@ -31,6 +31,11 @@ const frameNames = new Map([ /** this class encapsulates a frame of key-value bindings to be drawn on canvas */ export class Frame extends Visible implements IHoverable { + private static envFrameMap: Map = new Map(); + public static getFrom(environment: Env): Frame | undefined { + return Frame.envFrameMap.get(environment.id); + } + /** total height = frame height + frame title height */ readonly totalHeight: number; /** width of this frame + max width of the bound values */ @@ -57,6 +62,7 @@ export class Frame extends Visible implements IHoverable { this._width = Config.FrameMinWidth; this.level = envTreeNode.level as Level; this.environment = envTreeNode.environment; + Frame.envFrameMap.set(this.environment.id, this); this.parentFrame = envTreeNode.parent?.frame; this._x = this.level.x(); // derive the x coordinate from the left sibling frame @@ -112,6 +118,7 @@ export class Frame extends Visible implements IHoverable { } } } + let i = 0; // Add dummy bindings to `entries` for (const value of setDifference(unreferencedValues, nestedArrays)) { const descriptor: TypedPropertyDescriptor & PropertyDescriptor = { @@ -122,7 +129,7 @@ export class Frame extends Visible implements IHoverable { }; // The key is a number string to "disguise" as a dummy binding // TODO: revamp the dummy binding behavior, don't rely on numeric keys - entries.push(['0', descriptor]); + entries.push([`${i++}`, descriptor]); } for (const [key, data] of entries) { diff --git a/src/features/cseMachine/components/values/ArrayValue.tsx b/src/features/cseMachine/components/values/ArrayValue.tsx index 8701a3efb6..efb50f10a1 100644 --- a/src/features/cseMachine/components/values/ArrayValue.tsx +++ b/src/features/cseMachine/components/values/ArrayValue.tsx @@ -15,6 +15,7 @@ import { Value } from './Value'; export class ArrayValue extends Value { /** array of units this array is made of */ units: ArrayUnit[] = []; + unreferenced: boolean; constructor( /** underlying values this array contains */ @@ -23,11 +24,12 @@ export class ArrayValue extends Value { firstReference: ReferenceType ) { super(); - Layout.memoizeValue(this); + this.unreferenced = firstReference instanceof Binding && firstReference.isDummyBinding; this.addReference(firstReference); } handleNewReference(newReference: ReferenceType): void { + if (this.unreferenced && this.references.length > 0) this.unreferenced = false; if (!isMainReference(this, newReference)) return; // derive the coordinates from the main reference (binding / array unit) diff --git a/src/features/cseMachine/components/values/FnValue.tsx b/src/features/cseMachine/components/values/FnValue.tsx index 38aab1170a..979c547319 100644 --- a/src/features/cseMachine/components/values/FnValue.tsx +++ b/src/features/cseMachine/components/values/FnValue.tsx @@ -11,18 +11,18 @@ import { import CseMachine from '../../CseMachine'; import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; -import { Closure, EnvTreeNode, IHoverable, ReferenceType } from '../../CseMachineTypes'; +import { Closure, IHoverable, ReferenceType } from '../../CseMachineTypes'; import { defaultSAColor, + fadedSAColor, getBodyText, - getNonEmptyEnv, getParamsText, getTextWidth, - isEmptyEnvironment, isMainReference } from '../../CseMachineUtils'; import { ArrowFromFn } from '../arrows/ArrowFromFn'; import { Binding } from '../Binding'; +import { Frame } from '../Frame'; import { Value } from './Value'; /** this class encapsulates a JS Slang function (not from the global frame) that @@ -43,10 +43,12 @@ export class FnValue extends Value implements IHoverable { exportTooltipWidth!: number; private _arrow: ArrowFromFn | undefined; - /** the parent/enclosing environment of this fn value */ - enclosingEnvNode!: EnvTreeNode; + /** the enclosing frame of this fn value */ + enclosingFrame?: Frame; readonly labelRef: RefObject = React.createRef(); + unreferenced: boolean; + constructor( /** underlying JS Slang function (contains extra props) */ readonly data: Closure, @@ -54,20 +56,16 @@ export class FnValue extends Value implements IHoverable { 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); + this.unreferenced = firstReference instanceof Binding && firstReference.isDummyBinding; this.addReference(firstReference); } handleNewReference(newReference: ReferenceType): void { + if (this.unreferenced && this.references.length > 0) this.unreferenced = false; 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,9 +84,7 @@ 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.enclosingFrame = Frame.getFrom(this.data.environment); this.fnName = this.data.functionName; this.paramsText = `params: (${getParamsText(this.data)})`; @@ -122,9 +118,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.unreferenced ? fadedSAColor() : defaultSAColor(); return ( {CseMachine.getPrintableMode() ? ( diff --git a/src/features/cseMachine/components/values/GlobalFnValue.tsx b/src/features/cseMachine/components/values/GlobalFnValue.tsx index 1f88b826ae..5443f59a8a 100644 --- a/src/features/cseMachine/components/values/GlobalFnValue.tsx +++ b/src/features/cseMachine/components/values/GlobalFnValue.tsx @@ -11,7 +11,7 @@ import { import CseMachine from '../../CseMachine'; import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; -import { GlobalFn, IHoverable } from '../../CseMachineTypes'; +import { GlobalFn, IHoverable, ReferenceType } from '../../CseMachineTypes'; import { defaultSAColor, getBodyText, getParamsText, getTextWidth } from '../../CseMachineUtils'; import { ArrowFromFn } from '../arrows/ArrowFromFn'; import { Binding } from '../Binding'; @@ -40,17 +40,28 @@ export class GlobalFnValue extends Value implements IHoverable { /** underlying function */ readonly data: GlobalFn, /** what this value is being referenced by */ - mainReference: Binding + mainReference: ReferenceType ) { super(); - Layout.memoizeValue(this); this.references = [mainReference]; // derive the coordinates from the main reference (binding) - this._x = mainReference.frame.x() + mainReference.frame.width() + Config.FrameMarginX / 4; - this._y = mainReference.y(); - this.centerX = this._x + this.radius * 2; - this._y += this.radius; + if (mainReference instanceof Binding) { + this._x = mainReference.frame.x() + mainReference.frame.width() + Config.FrameMarginX / 2; + this._y = mainReference.y(); + this.centerX = this._x + this.radius * 2; + this._y += this.radius; + } else { + if (mainReference.isLastUnit) { + this._x = mainReference.x() + Config.DataUnitWidth * 2; + this._y = mainReference.y() + Config.DataUnitHeight / 2 - this.radius; + } else { + this._x = mainReference.x(); + this._y = mainReference.y() + mainReference.parent.height() + Config.DataUnitHeight; + } + this.centerX = this._x + Config.DataUnitWidth / 2; + this._x = this.centerX - this.radius * 2; + } this._width = this.radius * 4; this._height = this.radius * 2; 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(); From 885f7ca1d305f3d8c236c5713172f742637e0369 Mon Sep 17 00:00:00 2001 From: CZX Date: Sun, 7 Apr 2024 16:58:46 +0800 Subject: [PATCH 2/9] More fixes and added faded gc objects --- src/commons/utils/JsSlangHelper.ts | 1 - src/features/cseMachine/CseMachineConfig.ts | 4 +- src/features/cseMachine/CseMachineLayout.tsx | 69 +++++++------- src/features/cseMachine/CseMachineTypes.ts | 28 ++++-- src/features/cseMachine/CseMachineUtils.ts | 91 ++++++++++++------- .../cseMachine/components/ArrayUnit.tsx | 22 ++++- .../cseMachine/components/Binding.tsx | 6 +- src/features/cseMachine/components/Frame.tsx | 38 +++++--- .../components/StashItemComponent.tsx | 5 +- src/features/cseMachine/components/Text.tsx | 14 ++- .../components/arrows/ArrowFromArrayUnit.tsx | 5 + .../components/arrows/ArrowFromFn.tsx | 5 + .../components/arrows/GenericArrow.tsx | 9 +- .../components/values/ArrayValue.tsx | 17 +++- .../cseMachine/components/values/FnValue.tsx | 16 ++-- .../components/values/GlobalFnValue.tsx | 61 ++++++------- .../components/values/PrimitiveValue.tsx | 9 ++ .../cseMachine/components/values/Value.tsx | 9 ++ 18 files changed, 256 insertions(+), 153 deletions(-) diff --git a/src/commons/utils/JsSlangHelper.ts b/src/commons/utils/JsSlangHelper.ts index 37d6f0b23d..1dc0826f18 100644 --- a/src/commons/utils/JsSlangHelper.ts +++ b/src/commons/utils/JsSlangHelper.ts @@ -95,7 +95,6 @@ export function visualizeCseMachine({ context }: { context: Context }) { CseMachine.drawCse(context); } catch (err) { console.error(err); - throw new Error('CSE machine is not enabled'); } } diff --git a/src/features/cseMachine/CseMachineConfig.ts b/src/features/cseMachine/CseMachineConfig.ts index db925db1b9..761c6c2947 100644 --- a/src/features/cseMachine/CseMachineConfig.ts +++ b/src/features/cseMachine/CseMachineConfig.ts @@ -46,9 +46,9 @@ export const Config = Object.freeze({ MaxExportHeight: 12000, SA_WHITE: '#999999', - SA_FADED_WHITE: '#4d5c6b', + SA_FADED_WHITE: '#5b6773', SA_BLUE: '#2c3e50', - SA_FADED_BLUE: '#6e8faf', + 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 0e212e7140..349d970118 100644 --- a/src/features/cseMachine/CseMachineLayout.tsx +++ b/src/features/cseMachine/CseMachineLayout.tsx @@ -19,21 +19,24 @@ import CseMachine from './CseMachine'; import { CseAnimation } from './CseMachineAnimation'; import { Config, ShapeDefaultProps } from './CseMachineConfig'; import { - Closure, Data, DataArray, EnvTree, EnvTreeNode, GlobalFn, + NonGlobalFn, ReferenceType } from './CseMachineTypes'; import { assert, deepCopyTree, getNextChildren, + isBuiltInFn, isClosure, isDataArray, + isEnvEqual, isGlobalFn, + isNonGlobalFn, isPrimitiveData, isStreamFn, isUnassigned, @@ -77,7 +80,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; @@ -198,37 +201,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 }); - preludeEnv.heap.move(value, globalEnv.heap); - // globalEnv.heap.add(value); - const key = preludeValueKeyMap.get(value); - if (key) { - delete preludeEnv.head[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; }); } /** remove any global functions not referenced elsewhere in the program */ private static removeUnreferencedGlobalFns(): void { - const referencedFns = new Set(); + const referencedFns = new Set(); const visitedData = new Set(); const findGlobalFnReferences = (envNode: EnvTreeNode): void => { @@ -272,9 +280,9 @@ export class Layout { } // Then, find any references within any arrays inside the global environment heap, - // and also add any closures created in the global frame + // and also add any non-global functions created in the global frame for (const data of Layout.globalEnvNode.environment.heap.getHeap()) { - if (isClosure(data)) { + if (isNonGlobalFn(data)) { referencedFns.add(data); } else if (isDataArray(data)) { findGlobalFnReferencesInData(data); @@ -291,14 +299,8 @@ export class Layout { const newHead = {}; const newHeap = new Heap(); for (const fn of referencedFns) { - if (isGlobalFn(fn)) { - newHead[functionNames.get(fn)!] = fn; - if (fn.hasOwnProperty('environment')) { - newHeap.add(fn as Closure); - } - } else { - newHeap.add(fn); - } + if (isClosure(fn)) newHeap.add(fn); + if (isGlobalFn(fn)) newHead[functionNames.get(fn)!] = fn; } // add any arrays from the original heap to the new heap @@ -357,7 +359,9 @@ export class Layout { } else if (isPrimitiveData(data)) { return new PrimitiveValue(data, reference); } else { - 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; @@ -366,13 +370,14 @@ export class Layout { let newValue: Value = new PrimitiveValue(null, reference); if (isDataArray(data)) { newValue = new ArrayValue(data, reference); - } else if (isGlobalFn(data) || isStreamFn(data)) { + } else if (isGlobalFn(data)) { + assert(reference instanceof Binding); newValue = new GlobalFnValue(data, reference); } else { newValue = new FnValue(data, reference); } - Layout.values.set(data, newValue); + Layout.values.set(isBuiltInFn(data) || isStreamFn(data) ? data : data.id, newValue); return newValue; } } diff --git a/src/features/cseMachine/CseMachineTypes.ts b/src/features/cseMachine/CseMachineTypes.ts index f638285d76..03ad9a1b31 100644 --- a/src/features/cseMachine/CseMachineTypes.ts +++ b/src/features/cseMachine/CseMachineTypes.ts @@ -47,15 +47,24 @@ export type Unassigned = symbol; /** types of primitives in JS Slang */ export type Primitive = number | string | boolean | null | undefined; +/** fields of JS Slang closures */ +export type ClosureFields = + | 'id' + | 'environment' + | 'functionName' + | 'predefined' + | 'node' + | 'originalNode'; + +/** 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 = Exclude; +export type BuiltInFn = () => never; // Use `never` to differentiate from `StreamFn` /** types of pre-defined functions in JS Slang */ export type PredefinedFn = Omit & { predefined: true }; -/** types of global functions in JS Slang */ -export type GlobalFn = BuiltInFn | PredefinedFn; - /** * 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 @@ -63,10 +72,13 @@ export type GlobalFn = BuiltInFn | PredefinedFn; * * TODO: remove this and all other `StreamFn` code if `stream` becomes a pre-defined function */ -export type StreamFn = BuiltInFn & { environment: Env }; +export type StreamFn = (() => [any, StreamFn] | null) & { environment: Env }; -/** types of closures in JS Slang, redefined here for convenience. */ -export type Closure = JsSlangClosure; +/** 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[] & { @@ -75,7 +87,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 | 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 e459eae591..1d2fafa669 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, @@ -40,6 +41,7 @@ import { EnvTree, EnvTreeNode, GlobalFn, + NonGlobalFn, PredefinedFn, Primitive, ReferenceType, @@ -108,33 +110,49 @@ export function isFunction(x: any): x is Function { } /** Returns `true` if `data` is a built-in function */ -function isBuiltInFn(data: Data): data is BuiltInFn { - return isFunction(data) && !isClosure(data); +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 */ -function isPredefinedFn(data: Data): data is PredefinedFn { +export function isPredefinedFn(data: Data): data is PredefinedFn { return isClosure(data) && data.predefined; } -/** Returns `true` if `data` is a function in the global frame */ -export function isGlobalFn(data: Data): data is GlobalFn { - return isBuiltInFn(data) || isPredefinedFn(data); +/** Returns `true` if `data` is a JS Slang closure */ +export function isClosure(data: Data): data is Closure { + return ( + data instanceof JsSlangClosure || + (isFunction(data) && + {}.hasOwnProperty.call(data, 'environment') && + {}.hasOwnProperty.call(data, 'functionName') && + {}.hasOwnProperty.call(data, 'predefined')) + ); } -/** Returns `true` if `data` is a function returned from calling `stream` */ +/** + * 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 isBuiltInFn(data) && {}.hasOwnProperty.call(data, 'environment'); + return isFunction(data) && !isClosure(data) && {}.hasOwnProperty.call(data, 'environment'); } -/** Returns `true` if `data` is a JS Slang closure */ -export function isClosure(data: Data): data is Closure { - return ( - isFunction(data) && - {}.hasOwnProperty.call(data, 'environment') && - {}.hasOwnProperty.call(data, 'functionName') && - {}.hasOwnProperty.call(data, 'predefined') - ); +/** 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 */ @@ -192,12 +210,10 @@ export function setDifference(set1: Set, set2: Set) { /** * 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)) { @@ -206,21 +222,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 itself unreferenced. + */ +export function isDummyReference(reference: ReferenceType) { + return ( + (reference instanceof Binding && reference.isDummyBinding) || + (reference instanceof ArrayUnit && reference.unreferenced) ); - 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` */ @@ -299,17 +319,17 @@ export function getTextHeight( } /** Returns the parameter string of the given function */ -export function getParamsText(data: GlobalFn | Closure): string { +export function getParamsText(data: Closure | GlobalFn | StreamFn): string { if (isClosure(data)) { return data.node.params.map((node: any) => node.name).join(','); } else { const fnString = data.toString(); - return fnString.substring(fnString.indexOf('('), fnString.indexOf('{')).trim(); + return fnString.substring(fnString.indexOf('(') + 1, fnString.indexOf(')')).trim(); } } /** 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 = @@ -319,6 +339,9 @@ export function getBodyText(data: Function): string { 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('{')); } diff --git a/src/features/cseMachine/components/ArrayUnit.tsx b/src/features/cseMachine/components/ArrayUnit.tsx index 93c2763e7d..9ebb7ae54b 100644 --- a/src/features/cseMachine/components/ArrayUnit.tsx +++ b/src/features/cseMachine/components/ArrayUnit.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Config } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; import { Data } from '../CseMachineTypes'; -import { defaultSAColor, fadedSAColor } from '../CseMachineUtils'; +import { defaultSAColor, fadedSAColor, isDummyReference } from '../CseMachineUtils'; import { Arrow } from './arrows/Arrow'; import { ArrowFromArrayUnit } from './arrows/ArrowFromArrayUnit'; import { RoundedRect } from './shapes/RoundedRect'; @@ -26,6 +26,23 @@ export class ArrayUnit extends Visible { readonly isMainReference: boolean; arrow: Arrow | undefined = undefined; index: Text; + private _unreferenced: boolean = false; + get unreferenced() { + return this._unreferenced; + } + set unreferenced(value: boolean) { + if (value === this._unreferenced) return; + this._unreferenced = value; + if (value) { + // Only set unreferenced to true if all other references are also dummy references + if (this.value.references.filter(ref => !isDummyReference(ref)).length === 0) { + this.value.unreferenced = true; + } + } else { + this.value.unreferenced = false; + } + this.index.options.faded = value; + } constructor( /** index of this unit in its parent */ @@ -45,6 +62,7 @@ export class ArrayUnit extends Visible { this.value = Layout.createValue(this.data, this); this.isMainReference = this.value.references.length > 1; this.index = new Text(this.idx, this.x(), this.y() - 0.4 * this.height()); + this.unreferenced = parent.unreferenced; } updatePosition = () => {}; @@ -76,7 +94,7 @@ export class ArrayUnit extends Visible { y={this.y()} width={this.width()} height={this.height()} - stroke={this.parent.unreferenced ? fadedSAColor() : defaultSAColor()} + stroke={this.unreferenced ? fadedSAColor() : defaultSAColor()} hitStrokeWidth={Config.DataHitStrokeWidth} fillEnabled={false} onMouseEnter={this.onMouseEnter} diff --git a/src/features/cseMachine/components/Binding.tsx b/src/features/cseMachine/components/Binding.tsx index 2787d53a8f..f8cbbe4fcb 100644 --- a/src/features/cseMachine/components/Binding.tsx +++ b/src/features/cseMachine/components/Binding.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Config } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; import { Data } from '../CseMachineTypes'; -import { isDummyKey, isEnvEqual, isGlobalFn, isMainReference } from '../CseMachineUtils'; +import { isDummyKey, isMainReference } from '../CseMachineUtils'; import { ArrowFromText } from './arrows/ArrowFromText'; import { GenericArrow } from './arrows/GenericArrow'; import { Frame } from './Frame'; @@ -44,9 +44,7 @@ export class Binding extends Visible { readonly isConstant: boolean = false ) { super(); - this.isDummyBinding = - isDummyKey(this.keyString) || - (isEnvEqual(frame.environment, Layout.globalEnvNode.environment) && isGlobalFn(data)); + this.isDummyBinding = isDummyKey(this.keyString); // derive the coordinates from the binding above it if (this.prevBinding) { diff --git a/src/features/cseMachine/components/Frame.tsx b/src/features/cseMachine/components/Frame.tsx index fc6523d912..23de1f30f9 100644 --- a/src/features/cseMachine/components/Frame.tsx +++ b/src/features/cseMachine/components/Frame.tsx @@ -4,15 +4,15 @@ import { Group, Rect } from 'react-konva'; import CseMachine from '../CseMachine'; import { Config, ShapeDefaultProps } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; -import { DataArray, Env, EnvTreeNode, IHoverable } from '../CseMachineTypes'; +import { Env, EnvTreeNode, IHoverable } from '../CseMachineTypes'; import { currentItemSAColor, getTextWidth, getUnreferencedObjects, + isClosure, isDataArray, isPrimitiveData, - isUnassigned, - setDifference + isUnassigned } from '../CseMachineUtils'; import { ArrowFromFrame } from './arrows/ArrowFromFrame'; import { Binding } from './Binding'; @@ -101,26 +101,34 @@ export class Frame extends Visible implements IHoverable { const entries = Object.entries(Object.getOwnPropertyDescriptors(this.environment.head)); // get values that are unreferenced, which will used to created dummy bindings - const unreferencedValues = getUnreferencedObjects(this.environment); + const unreferencedValues = [...getUnreferencedObjects(this.environment)]; - // find arrays that are nested inside other arrays, and prevent them from creating new - // dummy bindings, as they should be drawn around the original parent array instead - const nestedArrays = new Set(); - for (const value of unreferencedValues) { + // TODO: find out why values are not added to heap on the correct order in JS Slang + // For now, sorting is a good workaround since id also increases in insertion order + unreferencedValues.sort((v1, v2) => Number(v1.id) - Number(v2.id)); + + // find objects that are nested inside other arrays, and prevent them from creating new + // dummy bindings by removing them from unreferencedValues, as they should be drawn + // around the original parent array instead + let i = 0; + while (i < unreferencedValues.length) { + const value = unreferencedValues[i]; if (isDataArray(value)) { for (const data of value) { - if (isDataArray(data) && data !== value) { - nestedArrays.add(data); - // Since deeply nested arrays always come first inside the heap order, there is no need - // to do a recursive search for deeply nested arrays, as they would have already been - // added to the nestedArrays set by this point. + if ((isDataArray(data) && data !== value) || isClosure(data)) { + const prev = unreferencedValues.findIndex(value => value.id === data.id); + if (prev > -1) { + unreferencedValues.splice(prev, 1); + if (prev <= i) i--; + } } } } + i++; } - let i = 0; + // Add dummy bindings to `entries` - for (const value of setDifference(unreferencedValues, nestedArrays)) { + for (const value of unreferencedValues) { const descriptor: TypedPropertyDescriptor & PropertyDescriptor = { value, configurable: false, diff --git a/src/features/cseMachine/components/StashItemComponent.tsx b/src/features/cseMachine/components/StashItemComponent.tsx index e14810fe95..d326956027 100644 --- a/src/features/cseMachine/components/StashItemComponent.tsx +++ b/src/features/cseMachine/components/StashItemComponent.tsx @@ -10,8 +10,8 @@ import { Layout } from '../CseMachineLayout'; import { IHoverable } from '../CseMachineTypes'; import { getTextWidth, - isClosure, isDataArray, + isNonGlobalFn, isStashItemInDanger, setHoveredCursor, setHoveredStyle, @@ -44,7 +44,7 @@ export class StashItemComponent extends Visible implements IHoverable { const valToStashRep = (val: any): string => { return typeof val === 'string' ? `'${val}'`.trim() - : isClosure(val) + : isNonGlobalFn(val) ? 'closure' : isDataArray(val) ? arrowTo @@ -69,6 +69,7 @@ export class StashItemComponent extends Visible implements IHoverable { this._x = ControlStashConfig.StashPosX + stackWidth; this._y = ControlStashConfig.StashPosY; if (arrowTo) { + arrowTo.unreferenced = false; 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..5b661fdfff 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.unreferenced; + } + 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..bd6266af3d 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.unreferenced; + } + 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..b21981cc25 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 0) this.unreferenced = false; + if (this.unreferenced) this.unreferenced = isDummyReference(newReference); if (!isMainReference(this, newReference)) return; // derive the coordinates from the main reference (binding / array unit) diff --git a/src/features/cseMachine/components/values/FnValue.tsx b/src/features/cseMachine/components/values/FnValue.tsx index 979c547319..9bb5107261 100644 --- a/src/features/cseMachine/components/values/FnValue.tsx +++ b/src/features/cseMachine/components/values/FnValue.tsx @@ -11,14 +11,16 @@ import { import CseMachine from '../../CseMachine'; import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; -import { Closure, IHoverable, ReferenceType } from '../../CseMachineTypes'; +import { IHoverable, NonGlobalFn, ReferenceType } from '../../CseMachineTypes'; import { defaultSAColor, fadedSAColor, getBodyText, getParamsText, getTextWidth, - isMainReference + isDummyReference, + isMainReference, + isStreamFn } from '../../CseMachineUtils'; import { ArrowFromFn } from '../arrows/ArrowFromFn'; import { Binding } from '../Binding'; @@ -47,21 +49,19 @@ export class FnValue extends Value implements IHoverable { enclosingFrame?: Frame; readonly labelRef: RefObject = React.createRef(); - unreferenced: boolean; - constructor( /** underlying JS Slang function (contains extra props) */ - readonly data: Closure, + readonly data: NonGlobalFn, /** what this value is being referenced by */ firstReference: ReferenceType ) { super(); - this.unreferenced = firstReference instanceof Binding && firstReference.isDummyBinding; + this.unreferenced = isDummyReference(firstReference); this.addReference(firstReference); } handleNewReference(newReference: ReferenceType): void { - if (this.unreferenced && this.references.length > 0) this.unreferenced = false; + if (this.unreferenced) this.unreferenced = isDummyReference(newReference); if (!isMainReference(this, newReference)) return; // derive the coordinates from the main reference (binding / array unit) if (newReference instanceof Binding) { @@ -85,7 +85,7 @@ export class FnValue extends Value implements IHoverable { this._height = this.radius * 2; this.enclosingFrame = Frame.getFrom(this.data.environment); - this.fnName = this.data.functionName; + this.fnName = isStreamFn(this.data) ? '' : this.data.functionName; this.paramsText = `params: (${getParamsText(this.data)})`; this.bodyText = `body: ${getBodyText(this.data)}`; diff --git a/src/features/cseMachine/components/values/GlobalFnValue.tsx b/src/features/cseMachine/components/values/GlobalFnValue.tsx index 5443f59a8a..e27ccd391f 100644 --- a/src/features/cseMachine/components/values/GlobalFnValue.tsx +++ b/src/features/cseMachine/components/values/GlobalFnValue.tsx @@ -11,8 +11,14 @@ import { import CseMachine from '../../CseMachine'; import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; -import { GlobalFn, IHoverable, ReferenceType } from '../../CseMachineTypes'; -import { defaultSAColor, getBodyText, getParamsText, getTextWidth } from '../../CseMachineUtils'; +import { GlobalFn, IHoverable } from '../../CseMachineTypes'; +import { + defaultSAColor, + fadedSAColor, + getBodyText, + getParamsText, + getTextWidth +} from '../../CseMachineUtils'; import { ArrowFromFn } from '../arrows/ArrowFromFn'; import { Binding } from '../Binding'; import { Value } from './Value'; @@ -40,33 +46,22 @@ export class GlobalFnValue extends Value implements IHoverable { /** underlying function */ readonly data: GlobalFn, /** what this value is being referenced by */ - mainReference: ReferenceType + mainReference: Binding ) { super(); + this.unreferenced = false; this.references = [mainReference]; // derive the coordinates from the main reference (binding) - if (mainReference instanceof Binding) { - this._x = mainReference.frame.x() + mainReference.frame.width() + Config.FrameMarginX / 2; - this._y = mainReference.y(); - this.centerX = this._x + this.radius * 2; - this._y += this.radius; - } else { - if (mainReference.isLastUnit) { - this._x = mainReference.x() + Config.DataUnitWidth * 2; - this._y = mainReference.y() + Config.DataUnitHeight / 2 - this.radius; - } else { - this._x = mainReference.x(); - this._y = mainReference.y() + mainReference.parent.height() + Config.DataUnitHeight; - } - this.centerX = this._x + Config.DataUnitWidth / 2; - this._x = this.centerX - this.radius * 2; - } + 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; this._width = this.radius * 4; this._height = this.radius * 2; - 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) @@ -75,18 +70,14 @@ 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 - } + handleNewReference(): void {} arrow(): ArrowFromFn | undefined { return this._arrow; @@ -119,15 +110,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.unreferenced ? fadedSAColor() : defaultSAColor(); return ( this.onMouseEnter(e)} onMouseLeave={e => this.onMouseLeave(e)} - onClick={e => this.onClick(e)} ref={this.ref} > {CseMachine.getPrintableMode() ? ( @@ -198,7 +189,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..a2afa69672 100644 --- a/src/features/cseMachine/components/values/PrimitiveValue.tsx +++ b/src/features/cseMachine/components/values/PrimitiveValue.tsx @@ -13,6 +13,14 @@ import { Value } from './Value'; export class PrimitiveValue extends Value { /** the text to be rendered */ readonly text: Text | ArrayNullUnit; + get unreferenced() { + return super.unreferenced; + } + set unreferenced(value: boolean) { + if (value === super.unreferenced) return; + super.unreferenced = value; + if (this.text instanceof Text) this.text.options.faded = value; + } constructor( /** data */ @@ -39,6 +47,7 @@ export class PrimitiveValue extends Value { maxWidth: maxWidth, isStringIdentifiable: true }); + this.unreferenced = reference.unreferenced; } this._width = this.text.width(); diff --git a/src/features/cseMachine/components/values/Value.tsx b/src/features/cseMachine/components/values/Value.tsx index d713793ebb..4e54a5306d 100644 --- a/src/features/cseMachine/components/values/Value.tsx +++ b/src/features/cseMachine/components/values/Value.tsx @@ -8,6 +8,15 @@ export abstract class Value extends Visible { /** the underlying data of this value */ abstract readonly data: Data; + /** if the value does not have active references, i.e. it should be garbage collected */ + private _unreferenced: boolean = false; + get unreferenced() { + return this._unreferenced; + } + set unreferenced(value: boolean) { + this._unreferenced = value; + } + /** references to this value */ public references: ReferenceType[] = []; From c1eefad463d85bc693c76dab54e1ff4bcb0ce613 Mon Sep 17 00:00:00 2001 From: CZX Date: Mon, 8 Apr 2024 08:08:06 +0800 Subject: [PATCH 3/9] Run format --- src/features/cseMachine/components/arrows/GenericArrow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/cseMachine/components/arrows/GenericArrow.tsx b/src/features/cseMachine/components/arrows/GenericArrow.tsx index b21981cc25..fe91a33530 100644 --- a/src/features/cseMachine/components/arrows/GenericArrow.tsx +++ b/src/features/cseMachine/components/arrows/GenericArrow.tsx @@ -21,7 +21,7 @@ export class GenericArrow exte this._x = from.x(); this._y = from.y(); } - + path(): string { return this._path; } From c49f3c226126ab9ea4c81867d359c2d8067a805d Mon Sep 17 00:00:00 2001 From: CZX Date: Mon, 8 Apr 2024 23:14:09 +0800 Subject: [PATCH 4/9] Fix params text and added SourceObject to display runes correctly --- src/features/cseMachine/CseMachineLayout.tsx | 8 +++++-- src/features/cseMachine/CseMachineTypes.ts | 15 +++++------- src/features/cseMachine/CseMachineUtils.ts | 24 +++++++++++++------ .../components/StashItemComponent.tsx | 3 +++ .../cseMachine/components/values/FnValue.tsx | 2 +- .../components/values/GlobalFnValue.tsx | 2 +- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/features/cseMachine/CseMachineLayout.tsx b/src/features/cseMachine/CseMachineLayout.tsx index 349d970118..4ea133550f 100644 --- a/src/features/cseMachine/CseMachineLayout.tsx +++ b/src/features/cseMachine/CseMachineLayout.tsx @@ -38,6 +38,7 @@ import { isGlobalFn, isNonGlobalFn, isPrimitiveData, + isSourceObject, isStreamFn, isUnassigned, setDifference @@ -296,11 +297,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 referencedFns) { if (isClosure(fn)) newHeap.add(fn); - if (isGlobalFn(fn)) newHead[functionNames.get(fn)!] = fn; + if (isGlobalFn(fn)) newHead[functionNames.get(fn) ?? `${i++}`] = fn; } // add any arrays from the original heap to the new heap @@ -373,8 +375,10 @@ export class Layout { } else if (isGlobalFn(data)) { assert(reference instanceof Binding); newValue = new GlobalFnValue(data, reference); - } else { + } else if (isNonGlobalFn(data)) { newValue = new FnValue(data, reference); + } else if (isSourceObject(data)) { + return new PrimitiveValue('<' + data.toReplString() + '>', reference); } Layout.values.set(isBuiltInFn(data) || isStreamFn(data) ? data : data.id, newValue); diff --git a/src/features/cseMachine/CseMachineTypes.ts b/src/features/cseMachine/CseMachineTypes.ts index 03ad9a1b31..eb812dc8a9 100644 --- a/src/features/cseMachine/CseMachineTypes.ts +++ b/src/features/cseMachine/CseMachineTypes.ts @@ -47,14 +47,11 @@ export type Unassigned = symbol; /** types of primitives in JS Slang */ export type Primitive = number | string | boolean | null | undefined; -/** fields of JS Slang closures */ -export type ClosureFields = - | 'id' - | 'environment' - | 'functionName' - | 'predefined' - | 'node' - | 'originalNode'; +/** types of source objects such as runes */ +export type SourceObject = { + [index: string]: any; + toReplString: () => string; +}; /** types of closures in JS Slang, redefined here for convenience. */ export type Closure = JsSlangClosure; @@ -87,7 +84,7 @@ export type DataArray = Data[] & { }; /** the types of data in the JS Slang context */ -export type Data = Primitive | NonGlobalFn | 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 1d2fafa669..73483ec97b 100644 --- a/src/features/cseMachine/CseMachineUtils.ts +++ b/src/features/cseMachine/CseMachineUtils.ts @@ -45,6 +45,7 @@ import { PredefinedFn, Primitive, ReferenceType, + SourceObject, StreamFn } from './CseMachineTypes'; @@ -70,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; @@ -121,14 +127,15 @@ 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 ( data instanceof JsSlangClosure || (isFunction(data) && - {}.hasOwnProperty.call(data, 'environment') && - {}.hasOwnProperty.call(data, 'functionName') && - {}.hasOwnProperty.call(data, 'predefined')) + closureFields.reduce((prev, field) => prev && obj.hasOwnProperty.call(data, field), true)) ); } @@ -318,13 +325,16 @@ export function getTextHeight( return numberOfLines * fontSize; } -/** Returns the parameter string of the given function */ +/** 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(); + console.log(params); + if (!params.startsWith('(')) params = '(' + params + ')'; + return params; } else { const fnString = data.toString(); - return fnString.substring(fnString.indexOf('(') + 1, fnString.indexOf(')')).trim(); + return fnString.substring(fnString.indexOf('('), fnString.indexOf(')') + 1); } } @@ -333,7 +343,7 @@ export function getBodyText(data: Closure | GlobalFn | StreamFn): string { const fnString = data.toString(); if (isClosure(data)) { let body = - data.node.type === 'FunctionExpression' || fnString.substring(0, 8) === 'function' + fnString.substring(0, 8) === 'function' ? fnString.substring(fnString.indexOf('{')) : fnString.substring(fnString.indexOf('=') + 3); diff --git a/src/features/cseMachine/components/StashItemComponent.tsx b/src/features/cseMachine/components/StashItemComponent.tsx index d326956027..9b253562a5 100644 --- a/src/features/cseMachine/components/StashItemComponent.tsx +++ b/src/features/cseMachine/components/StashItemComponent.tsx @@ -12,6 +12,7 @@ import { getTextWidth, isDataArray, isNonGlobalFn, + isSourceObject, isStashItemInDanger, setHoveredCursor, setHoveredStyle, @@ -50,6 +51,8 @@ export class StashItemComponent extends Visible implements IHoverable { ? arrowTo ? 'pair/array' : JSON.stringify(val) + : isSourceObject(val) + ? '<' + val.toReplString() + '>' : String(value); }; this.text = truncateText( diff --git a/src/features/cseMachine/components/values/FnValue.tsx b/src/features/cseMachine/components/values/FnValue.tsx index 9bb5107261..431718cc76 100644 --- a/src/features/cseMachine/components/values/FnValue.tsx +++ b/src/features/cseMachine/components/values/FnValue.tsx @@ -87,7 +87,7 @@ export class FnValue extends Value implements IHoverable { 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) diff --git a/src/features/cseMachine/components/values/GlobalFnValue.tsx b/src/features/cseMachine/components/values/GlobalFnValue.tsx index e27ccd391f..d8e461d15c 100644 --- a/src/features/cseMachine/components/values/GlobalFnValue.tsx +++ b/src/features/cseMachine/components/values/GlobalFnValue.tsx @@ -61,7 +61,7 @@ export class GlobalFnValue extends Value implements IHoverable { this._width = this.radius * 4; this._height = this.radius * 2; - 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) From 33f8ed677183599b0a44b292e6a37e0718d32f44 Mon Sep 17 00:00:00 2001 From: henz Date: Tue, 9 Apr 2024 08:19:08 +0800 Subject: [PATCH 5/9] bumping js-slang --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5d9eabb87a..934c2abba6 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "flexboxgrid-helpers": "^1.1.3", "hastscript": "^9.0.0", "java-slang": "^1.0.3", - "js-slang": "^1.0.62", + "js-slang": "^1.0.63", "js-yaml": "^4.1.0", "konva": "^9.2.0", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 08195900c6..6259e6a6ee 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.63: + version "1.0.63" + resolved "https://registry.yarnpkg.com/js-slang/-/js-slang-1.0.63.tgz#c586cc0bb7498326193e2f629e4cf283171868ed" + integrity sha512-shSxT7zxmi8xh3qy0WdR5MpJzBGqEs9I0ulXytkAYDFq8w6GyF3HWGBc0lPSrvm0iMx8/GekqMW8wmVmDKsQWA== dependencies: "@babel/parser" "^7.19.4" "@joeychenofficial/alt-ergo-modified" "^2.4.0" From c814cd92fca74a159d00da63612206aa5d36fad1 Mon Sep 17 00:00:00 2001 From: CZX Date: Tue, 9 Apr 2024 16:18:53 +0800 Subject: [PATCH 6/9] Revamp unreferenced behavior and update snapshots --- src/features/cseMachine/CseMachine.tsx | 4 +- src/features/cseMachine/CseMachineLayout.tsx | 15 ++++-- src/features/cseMachine/CseMachineUtils.ts | 54 ++++++------------- .../cseMachine/__tests__/CseMachine.tsx | 2 +- .../__snapshots__/CseMachine.tsx.snap | 2 +- .../cseMachine/components/ArrayEmptyUnit.tsx | 2 +- .../cseMachine/components/ArrayNullUnit.tsx | 2 +- .../cseMachine/components/ArrayUnit.tsx | 24 ++------- .../components/StashItemComponent.tsx | 4 +- .../components/arrows/ArrowFromArrayUnit.tsx | 2 +- .../components/arrows/ArrowFromFn.tsx | 2 +- .../components/values/ArrayValue.tsx | 24 ++++----- .../cseMachine/components/values/FnValue.tsx | 7 ++- .../components/values/GlobalFnValue.tsx | 7 +-- .../components/values/PrimitiveValue.tsx | 25 +++++---- .../cseMachine/components/values/Value.tsx | 21 +++++--- 16 files changed, 85 insertions(+), 112 deletions(-) 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/CseMachineLayout.tsx b/src/features/cseMachine/CseMachineLayout.tsx index 4ea133550f..1a3aac41f6 100644 --- a/src/features/cseMachine/CseMachineLayout.tsx +++ b/src/features/cseMachine/CseMachineLayout.tsx @@ -25,7 +25,8 @@ import { EnvTreeNode, GlobalFn, NonGlobalFn, - ReferenceType + ReferenceType, + StreamFn } from './CseMachineTypes'; import { assert, @@ -369,7 +370,7 @@ export class Layout { return existingValue; } - let newValue: Value = new PrimitiveValue(null, reference); + let newValue: Value | undefined; if (isDataArray(data)) { newValue = new ArrayValue(data, reference); } else if (isGlobalFn(data)) { @@ -378,14 +379,18 @@ export class Layout { } else if (isNonGlobalFn(data)) { newValue = new FnValue(data, reference); } else if (isSourceObject(data)) { - return new PrimitiveValue('<' + data.toReplString() + '>', reference); + return new PrimitiveValue(data.toReplString(), reference); } - Layout.values.set(isBuiltInFn(data) || isStreamFn(data) ? data : data.id, newValue); - 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/CseMachineUtils.ts b/src/features/cseMachine/CseMachineUtils.ts index 73483ec97b..1b9ed30437 100644 --- a/src/features/cseMachine/CseMachineUtils.ts +++ b/src/features/cseMachine/CseMachineUtils.ts @@ -240,13 +240,13 @@ export function isMainReference(value: Value, reference: ReferenceType) { } /** - * Returns `true` if `reference` is a dummy reference, i.e. it is a dummy binding, or the reference - * is itself unreferenced. + * 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.unreferenced) + (reference instanceof ArrayUnit && !reference.parent.isReferenced()) ); } @@ -264,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. * @@ -274,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; } @@ -310,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; } @@ -329,7 +326,6 @@ export function getTextHeight( export function getParamsText(data: Closure | GlobalFn | StreamFn): string { if (isClosure(data)) { let params = data.functionName.slice(0, data.functionName.indexOf('=>')).trim(); - console.log(params); if (!params.startsWith('(')) params = '(' + params + ')'; return params; } else { @@ -714,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 ) ); @@ -806,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 => { 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..0101886215 100644 --- a/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap +++ b/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap @@ -3528,7 +3528,7 @@ Array [ [Function], "x: ", 11, - "0:= ", + "1:= ", [Function], ] `; diff --git a/src/features/cseMachine/components/ArrayEmptyUnit.tsx b/src/features/cseMachine/components/ArrayEmptyUnit.tsx index b65c82e6bb..9a58c9ef07 100644 --- a/src/features/cseMachine/components/ArrayEmptyUnit.tsx +++ b/src/features/cseMachine/components/ArrayEmptyUnit.tsx @@ -29,7 +29,7 @@ export class ArrayEmptyUnit extends Visible { y={this.y()} width={this.width()} height={this.height()} - stroke={this.parent.unreferenced ? fadedSAColor() : defaultSAColor()} + stroke={this.parent.isReferenced() ? defaultSAColor() : fadedSAColor()} ref={this.ref} /> ); diff --git a/src/features/cseMachine/components/ArrayNullUnit.tsx b/src/features/cseMachine/components/ArrayNullUnit.tsx index 73bd1a37c3..39bfb00d71 100644 --- a/src/features/cseMachine/components/ArrayNullUnit.tsx +++ b/src/features/cseMachine/components/ArrayNullUnit.tsx @@ -23,7 +23,7 @@ export class ArrayNullUnit extends Visible { {...ShapeDefaultProps} key={Layout.key++} points={[this.x(), this.y() + this.height(), this.x() + this.width(), this.y()]} - stroke={this.reference.parent.unreferenced ? fadedSAColor() : defaultSAColor()} + stroke={this.reference.parent.isReferenced() ? defaultSAColor() : fadedSAColor()} hitStrokeWidth={Config.DataHitStrokeWidth} ref={this.ref} listening={false} diff --git a/src/features/cseMachine/components/ArrayUnit.tsx b/src/features/cseMachine/components/ArrayUnit.tsx index 9ebb7ae54b..99f570e111 100644 --- a/src/features/cseMachine/components/ArrayUnit.tsx +++ b/src/features/cseMachine/components/ArrayUnit.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Config } from '../CseMachineConfig'; import { Layout } from '../CseMachineLayout'; import { Data } from '../CseMachineTypes'; -import { defaultSAColor, fadedSAColor, isDummyReference } from '../CseMachineUtils'; +import { defaultSAColor, fadedSAColor } from '../CseMachineUtils'; import { Arrow } from './arrows/Arrow'; import { ArrowFromArrayUnit } from './arrows/ArrowFromArrayUnit'; import { RoundedRect } from './shapes/RoundedRect'; @@ -26,23 +26,6 @@ export class ArrayUnit extends Visible { readonly isMainReference: boolean; arrow: Arrow | undefined = undefined; index: Text; - private _unreferenced: boolean = false; - get unreferenced() { - return this._unreferenced; - } - set unreferenced(value: boolean) { - if (value === this._unreferenced) return; - this._unreferenced = value; - if (value) { - // Only set unreferenced to true if all other references are also dummy references - if (this.value.references.filter(ref => !isDummyReference(ref)).length === 0) { - this.value.unreferenced = true; - } - } else { - this.value.unreferenced = false; - } - this.index.options.faded = value; - } constructor( /** index of this unit in its parent */ @@ -61,8 +44,7 @@ export class ArrayUnit extends Visible { this.isLastUnit = this.idx === this.parent.data.length - 1; this.value = Layout.createValue(this.data, this); this.isMainReference = this.value.references.length > 1; - this.index = new Text(this.idx, this.x(), this.y() - 0.4 * this.height()); - this.unreferenced = parent.unreferenced; + this.index = new Text(this.idx, this.x(), this.y() - 0.4 * this.height(), { faded: true }); } updatePosition = () => {}; @@ -94,7 +76,7 @@ export class ArrayUnit extends Visible { y={this.y()} width={this.width()} height={this.height()} - stroke={this.unreferenced ? fadedSAColor() : defaultSAColor()} + stroke={this.parent.isReferenced() ? defaultSAColor() : fadedSAColor()} hitStrokeWidth={Config.DataHitStrokeWidth} fillEnabled={false} onMouseEnter={this.onMouseEnter} diff --git a/src/features/cseMachine/components/StashItemComponent.tsx b/src/features/cseMachine/components/StashItemComponent.tsx index 9b253562a5..5f8331a7e5 100644 --- a/src/features/cseMachine/components/StashItemComponent.tsx +++ b/src/features/cseMachine/components/StashItemComponent.tsx @@ -52,7 +52,7 @@ export class StashItemComponent extends Visible implements IHoverable { ? 'pair/array' : JSON.stringify(val) : isSourceObject(val) - ? '<' + val.toReplString() + '>' + ? val.toReplString() : String(value); }; this.text = truncateText( @@ -72,7 +72,7 @@ export class StashItemComponent extends Visible implements IHoverable { this._x = ControlStashConfig.StashPosX + stackWidth; this._y = ControlStashConfig.StashPosY; if (arrowTo) { - arrowTo.unreferenced = false; + arrowTo.markAsReferenced(); this.arrow = new ArrowFromStashItemComponent(this).to(arrowTo) as ArrowFromStashItemComponent; } } diff --git a/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx b/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx index 5b661fdfff..b670f06e4a 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx @@ -11,7 +11,7 @@ import { GenericArrow } from './GenericArrow'; export class ArrowFromArrayUnit extends GenericArrow { constructor(from: ArrayUnit) { super(from); - this.faded = from.parent.unreferenced; + this.faded = !from.parent.isReferenced(); } protected calculateSteps() { diff --git a/src/features/cseMachine/components/arrows/ArrowFromFn.tsx b/src/features/cseMachine/components/arrows/ArrowFromFn.tsx index bd6266af3d..b5eb1cbf1b 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromFn.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromFn.tsx @@ -9,7 +9,7 @@ import { GenericArrow } from './GenericArrow'; export class ArrowFromFn extends GenericArrow { constructor(from: FnValue | GlobalFnValue) { super(from); - this.faded = from.unreferenced; + this.faded = !from.isReferenced(); } protected calculateSteps() { diff --git a/src/features/cseMachine/components/values/ArrayValue.tsx b/src/features/cseMachine/components/values/ArrayValue.tsx index a4c39122c5..d571908aff 100644 --- a/src/features/cseMachine/components/values/ArrayValue.tsx +++ b/src/features/cseMachine/components/values/ArrayValue.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Config } from '../../CseMachineConfig'; import { Layout } from '../../CseMachineLayout'; import { DataArray, ReferenceType } from '../../CseMachineTypes'; -import { isDummyReference, isMainReference } from '../../CseMachineUtils'; +import { isMainReference } from '../../CseMachineUtils'; import { ArrayEmptyUnit } from '../ArrayEmptyUnit'; import { ArrayUnit } from '../ArrayUnit'; import { Binding } from '../Binding'; @@ -15,16 +15,6 @@ import { Value } from './Value'; export class ArrayValue extends Value { /** array of units this array is made of */ units: ArrayUnit[] = []; - get unreferenced() { - return super.unreferenced; - } - set unreferenced(value: boolean) { - if (value === super.unreferenced) return; - super.unreferenced = value; - for (const unit of this.units) { - unit.unreferenced = value; - } - } constructor( /** underlying values this array contains */ @@ -33,12 +23,11 @@ export class ArrayValue extends Value { firstReference: ReferenceType ) { super(); - this.unreferenced = isDummyReference(firstReference); + Layout.memoizeValue(data, this); this.addReference(firstReference); } handleNewReference(newReference: ReferenceType): void { - if (this.unreferenced) this.unreferenced = isDummyReference(newReference); if (!isMainReference(this, newReference)) return; // derive the coordinates from the main reference (binding / array unit) @@ -86,6 +75,15 @@ export class ArrayValue extends Value { } } + markAsReferenced() { + if (this.isReferenced()) return; + super.markAsReferenced(); + for (const unit of this.units) { + unit.index.options.faded = false; + unit.value.markAsReferenced(); + } + } + draw(): React.ReactNode { if (this.isDrawn()) return null; this._isDrawn = true; diff --git a/src/features/cseMachine/components/values/FnValue.tsx b/src/features/cseMachine/components/values/FnValue.tsx index 431718cc76..73ab794d41 100644 --- a/src/features/cseMachine/components/values/FnValue.tsx +++ b/src/features/cseMachine/components/values/FnValue.tsx @@ -18,7 +18,6 @@ import { getBodyText, getParamsText, getTextWidth, - isDummyReference, isMainReference, isStreamFn } from '../../CseMachineUtils'; @@ -56,13 +55,13 @@ export class FnValue extends Value implements IHoverable { firstReference: ReferenceType ) { super(); - this.unreferenced = isDummyReference(firstReference); + Layout.memoizeValue(data, this); this.addReference(firstReference); } handleNewReference(newReference: ReferenceType): void { - if (this.unreferenced) this.unreferenced = isDummyReference(newReference); 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; @@ -121,7 +120,7 @@ export class FnValue extends Value implements IHoverable { if (this.enclosingFrame) { this._arrow = new ArrowFromFn(this).to(this.enclosingFrame) as ArrowFromFn; } - const stroke = this.unreferenced ? fadedSAColor() : defaultSAColor(); + const stroke = this.isReferenced() ? defaultSAColor() : fadedSAColor(); return ( 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/Value.tsx b/src/features/cseMachine/components/values/Value.tsx index 4e54a5306d..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,13 +9,18 @@ export abstract class Value extends Visible { /** the underlying data of this value */ abstract readonly data: Data; - /** if the value does not have active references, i.e. it should be garbage collected */ - private _unreferenced: boolean = false; - get unreferenced() { - return this._unreferenced; + /** + * 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; } - set unreferenced(value: boolean) { - this._unreferenced = value; + + markAsReferenced() { + this._isReferenced = true; } /** references to this value */ @@ -24,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 */ From 0d6202cf6c7662431f5906ecbdbca8b71b69a7dd Mon Sep 17 00:00:00 2001 From: CZX Date: Tue, 9 Apr 2024 16:19:31 +0800 Subject: [PATCH 7/9] Run format --- src/features/cseMachine/components/values/GlobalFnValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/cseMachine/components/values/GlobalFnValue.tsx b/src/features/cseMachine/components/values/GlobalFnValue.tsx index 6d8b593fd4..5bd18587b0 100644 --- a/src/features/cseMachine/components/values/GlobalFnValue.tsx +++ b/src/features/cseMachine/components/values/GlobalFnValue.tsx @@ -74,7 +74,7 @@ export class GlobalFnValue extends Value implements IHoverable { getTextWidth(this.paramsText), getTextWidth(this.exportBodyText) ); - + this.addReference(mainReference); } From bd82d0bb8da7771a39d7e21ddb2d39960e03824b Mon Sep 17 00:00:00 2001 From: CZX Date: Tue, 9 Apr 2024 16:31:57 +0800 Subject: [PATCH 8/9] Update snapshot --- .../__snapshots__/CseMachine.tsx.snap | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap b/src/features/cseMachine/__tests__/__snapshots__/CseMachine.tsx.snap index 0101886215..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]} >