Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSE Machine: Fixes for variadic function & global closures, added faded unreferenced objects #2906

Merged
merged 14 commits into from
Apr 9, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@
"flexboxgrid-helpers": "^1.1.3",
"hastscript": "^9.0.0",
"java-slang": "^1.0.4",
"js-slang": "^1.0.62",
"js-slang": "^1.0.64",
"js-yaml": "^4.1.0",
"konva": "^9.2.0",
"lodash": "^4.17.21",
2 changes: 1 addition & 1 deletion src/commons/utils/JsSlangHelper.ts
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ export function visualizeCseMachine({ context }: { context: Context }) {
try {
CseMachine.drawCse(context);
} catch (err) {
throw new Error('CSE machine is not enabled');
console.error(err);
}
}

4 changes: 2 additions & 2 deletions src/features/cseMachine/CseMachine.tsx
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/features/cseMachine/CseMachineConfig.ts
Original file line number Diff line number Diff line change
@@ -46,7 +46,9 @@ export const Config = Object.freeze({
MaxExportHeight: 12000,

SA_WHITE: '#999999',
SA_FADED_WHITE: '#5b6773',
SA_BLUE: '#2c3e50',
SA_FADED_BLUE: '#BBB',
PRINT_BACKGROUND: 'white',
SA_CURRENT_ITEM: '#030fff',

132 changes: 65 additions & 67 deletions src/features/cseMachine/CseMachineLayout.tsx
Original file line number Diff line number Diff line change
@@ -19,23 +19,28 @@ import CseMachine from './CseMachine';
import { CseAnimation } from './CseMachineAnimation';
import { Config, ShapeDefaultProps } from './CseMachineConfig';
import {
Closure,
Data,
DataArray,
EnvTree,
EnvTreeNode,
GlobalFn,
ReferenceType
NonGlobalFn,
ReferenceType,
StreamFn
} from './CseMachineTypes';
import {
convertClosureToGlobalFn,
assert,
deepCopyTree,
getNextChildren,
isBuiltInFn,
isClosure,
isDataArray,
isFunction,
isEnvEqual,
isGlobalFn,
isNonGlobalFn,
isPrimitiveData,
isSourceObject,
isStreamFn,
isUnassigned,
setDifference
} from './CseMachineUtils';
@@ -63,8 +68,6 @@ export class Layout {
/** scale factor for zooming and out of canvas */
static scaleFactor = 1.02;

/** the environment tree */
static environmentTree: EnvTree;
/** the global environment */
static globalEnvNode: EnvTreeNode;
/** grid of frames */
@@ -79,7 +82,7 @@ export class Layout {
static previousStashComponent: StashStack;

/** memoized values */
static values = new Map<Data, Value>();
static values = new Map<string | (() => any), Value>();
/** memoized layout */
static prevLayout: React.ReactNode;
static currentDark: React.ReactNode;
@@ -140,8 +143,7 @@ export class Layout {
Layout.key = 0;

// deep copy so we don't mutate the context
Layout.environmentTree = deepCopyTree(envTree);
Layout.globalEnvNode = Layout.environmentTree.root;
Layout.globalEnvNode = deepCopyTree(envTree).root;
Layout.control = control;
Layout.stash = stash;

@@ -201,50 +203,50 @@ export class Layout {

const preludeEnvNode = Layout.globalEnvNode.children[0];
const preludeEnv = preludeEnvNode.environment;
const globalEnvNode = Layout.globalEnvNode;
const globalEnv = globalEnvNode.environment;

const preludeValueKeyMap = new Map(
Object.entries(preludeEnv.head).map(([key, value]) => [value, key])
);
const globalEnv = Layout.globalEnvNode.environment;

// Add bindings from prelude environment head to global environment head
for (const [key, value] of Object.entries(preludeEnv.head)) {
delete preludeEnv.head[key];
globalEnv.head[key] = value;
if (isStreamFn(value) && isEnvEqual(value.environment, preludeEnv)) {
Object.defineProperty(value, 'environment', { value: globalEnv });
}
}

// Change environments of each array and closure in the prelude to be the global environment
// Move objects from prelude environment heap to global environment heap
for (const value of preludeEnv.heap.getHeap()) {
Object.defineProperty(value, 'environment', { value: globalEnvNode.environment });
globalEnv.heap.add(value);
const key = preludeValueKeyMap.get(value);
if (key) {
globalEnv.head[key] = value;
Object.defineProperty(value, 'environment', { value: globalEnv });
if (isDataArray(value)) {
for (const item of value) {
if (isStreamFn(item) && isEnvEqual(item.environment, preludeEnv)) {
Object.defineProperty(item, 'environment', { value: globalEnv });
}
}
}
preludeEnv.heap.move(value, globalEnv.heap);
}

// update globalEnvNode children
globalEnvNode.resetChildren(preludeEnvNode.children);
Layout.globalEnvNode.resetChildren(preludeEnvNode.children);

// update the tail of each child's environment to point to the global environment
globalEnvNode.children.forEach(node => {
Layout.globalEnvNode.children.forEach(node => {
node.environment.tail = globalEnv;
});

// go through new bindings and update closures to be global functions
for (const value of Object.values(globalEnv.head)) {
if (isClosure(value)) {
convertClosureToGlobalFn(value);
}
}
}

/** remove any global functions not referenced elsewhere in the program */
private static removeUnreferencedGlobalFns(): void {
const referencedGlobalFns = new Set<GlobalFn>();
const referencedFns = new Set<GlobalFn | NonGlobalFn>();
const visitedData = new Set<DataArray>();

const findGlobalFnReferences = (envNode: EnvTreeNode): void => {
const headValues = Object.values(envNode.environment.head);
const unreferenced = setDifference(envNode.environment.heap.getHeap(), new Set(headValues));
for (const data of headValues) {
if (isGlobalFn(data)) {
referencedGlobalFns.add(data);
referencedFns.add(data);
} else if (isDataArray(data)) {
findGlobalFnReferencesInData(data);
}
@@ -263,7 +265,7 @@ export class Layout {
visitedData.add(data);
data.forEach(d => {
if (isGlobalFn(d)) {
referencedGlobalFns.add(d);
referencedFns.add(d);
} else if (isDataArray(d)) {
findGlobalFnReferencesInData(d);
}
@@ -273,15 +275,18 @@ export class Layout {
// First, add any referenced global functions in the stash
for (const item of Layout.stash.getStack()) {
if (isGlobalFn(item)) {
referencedGlobalFns.add(item);
referencedFns.add(item);
} else if (isDataArray(item)) {
findGlobalFnReferencesInData(item);
}
}

// Then, find any references within any arrays inside the global environment heap
// Then, find any references within any arrays inside the global environment heap,
// and also add any non-global functions created in the global frame
for (const data of Layout.globalEnvNode.environment.heap.getHeap()) {
if (isDataArray(data)) {
if (isNonGlobalFn(data)) {
referencedFns.add(data);
} else if (isDataArray(data)) {
findGlobalFnReferencesInData(data);
}
}
@@ -293,13 +298,12 @@ export class Layout {
Object.entries(Layout.globalEnvNode.environment.head).map(([key, value]) => [value, key])
);

let i = 0;
const newHead = {};
const newHeap = new Heap();
for (const fn of referencedGlobalFns) {
newHead[functionNames.get(fn)!] = fn;
if (fn.hasOwnProperty('environment')) {
newHeap.add(fn as Closure);
}
for (const fn of referencedFns) {
if (isClosure(fn)) newHeap.add(fn);
if (isGlobalFn(fn)) newHead[functionNames.get(fn) ?? `${i++}`] = fn;
}

// add any arrays from the original heap to the new heap
@@ -349,50 +353,44 @@ export class Layout {
}
}

/** memoize `Value` (used to detect circular references in non-primitive `Value`) */
static memoizeValue(value: Value): void {
Layout.values.set(value.data, value);
}

/** create an instance of the corresponding `Value` if it doesn't already exists,
* else, return the existing value */
/** Creates an instance of the corresponding `Value` if it doesn't already exists,
* else, returns the existing value */
static createValue(data: Data, reference: ReferenceType): Value {
if (isUnassigned(data)) {
assert(reference instanceof Binding);
return new UnassignedValue(reference);
} else if (isPrimitiveData(data)) {
return new PrimitiveValue(data, reference);
} else {
// try to find if this value is already created
const existingValue = Layout.values.get(data);
const existingValue = Layout.values.get(
isBuiltInFn(data) || isStreamFn(data) ? data : data.id
);
if (existingValue) {
existingValue.addReference(reference);
return existingValue;
}

// else create a new one
let newValue: Value = new PrimitiveValue(null, reference);
let newValue: Value | undefined;
if (isDataArray(data)) {
newValue = new ArrayValue(data, reference);
} else if (isFunction(data)) {
if (isClosure(data)) {
// normal JS Slang function
newValue = new FnValue(data, reference);
} else {
if (reference instanceof Binding) {
// function from the global env (has no extra props such as env, fnName)
newValue = new GlobalFnValue(data, reference);
} else {
// this should be impossible, since bindings for global function always get
// drawn first, before any other values like arrays get drawn
throw new Error('First reference of global function value is not a binding!');
}
}
} else if (isGlobalFn(data)) {
assert(reference instanceof Binding);
newValue = new GlobalFnValue(data, reference);
} else if (isNonGlobalFn(data)) {
newValue = new FnValue(data, reference);
} else if (isSourceObject(data)) {
return new PrimitiveValue(data.toReplString(), reference);
}

return newValue;
return newValue ?? new PrimitiveValue(null, reference);
}
}

static memoizeValue(data: GlobalFn | NonGlobalFn | StreamFn | DataArray, value: Value) {
if (isBuiltInFn(data) || isStreamFn(data)) Layout.values.set(data, value);
else Layout.values.set(data.id, value);
}

/**
* Scrolls diagram to top left, resets the zoom, and saves the diagram as multiple images of width < MaxExportWidth.
*/
34 changes: 29 additions & 5 deletions src/features/cseMachine/CseMachineTypes.ts
Original file line number Diff line number Diff line change
@@ -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,20 +47,44 @@ export type Unassigned = symbol;
/** types of primitives in JS Slang */
export type Primitive = number | string | boolean | null | undefined;

/** types of in-built functions in JS Slang */
export type GlobalFn = Function;
/** types of source objects such as runes */
export type SourceObject = {
[index: string]: any;
toReplString: () => string;
};

/** types of functions in JS Slang */
/** types of closures in JS Slang, redefined here for convenience. */
export type Closure = JsSlangClosure;

/** types of built-in functions in JS Slang */
export type BuiltInFn = () => never; // Use `never` to differentiate from `StreamFn`

/** types of pre-defined functions in JS Slang */
export type PredefinedFn = Omit<Closure, 'predefined'> & { predefined: true };

/**
* Special type of a function returned from calling `stream`. It is mostly similar to a global
* function, but has the extra `environment` property as it should be drawn next to the frame
* in which `stream` is called.
*
* TODO: remove this and all other `StreamFn` code if `stream` becomes a pre-defined function
*/
export type StreamFn = (() => [any, StreamFn] | null) & { environment: Env };

/** types of global functions in JS Slang */
export type GlobalFn = BuiltInFn | PredefinedFn;

/** types of global functions in JS Slang */
export type NonGlobalFn = (Omit<Closure, 'predefined'> & { predefined: false }) | StreamFn;

/** types of arrays in JS Slang */
export type DataArray = Data[] & {
readonly id: string;
environment: Env;
};

/** the types of data in the JS Slang context */
export type Data = Primitive | Closure | GlobalFn | Unassigned | DataArray;
export type Data = Primitive | SourceObject | NonGlobalFn | GlobalFn | Unassigned | DataArray;

/** modified `Environment` to store children and associated frame */
export type Env = Environment;
Loading
Loading