diff --git a/package.json b/package.json index c0714545bc..82fadd2c5e 100644 --- a/package.json +++ b/package.json @@ -33,49 +33,49 @@ "@mantine/hooks": "^7.11.2", "@octokit/rest": "^20.0.0", "@reduxjs/toolkit": "^1.9.7", - "@sentry/browser": "^7.57.0", - "@sourceacademy/c-slang": "^1.0.20", + "@sentry/browser": "^8.33.0", + "@sourceacademy/c-slang": "^1.0.21", "@sourceacademy/sharedb-ace": "^2.0.3", "@sourceacademy/sling-client": "^0.1.0", "@szhsin/react-menu": "^4.0.0", "@tanstack/react-table": "^8.9.3", "@tremor/react": "^1.8.2", - "ace-builds": "^1.4.14", + "ace-builds": "^1.36.3", "acorn": "^8.9.0", - "ag-grid-community": "^32.0.2", - "ag-grid-react": "^32.0.2", + "ag-grid-community": "^32.3.1", + "ag-grid-react": "^32.3.1", "array-move": "^4.0.0", "browserfs": "^1.4.3", "classnames": "^2.3.2", + "dayjs": "^1.11.13", "dompurify": "^3.1.6", "flexboxgrid": "^6.3.1", "flexboxgrid-helpers": "^1.1.3", "hastscript": "^9.0.0", "i18next": "^23.11.2", - "i18next-browser-languagedetector": "^7.2.1", + "i18next-browser-languagedetector": "^8.0.0", "java-slang": "^1.0.13", "js-cookie": "^3.0.5", - "js-slang": "^1.0.76", + "js-slang": "^1.0.79", "js-yaml": "^4.1.0", "konva": "^9.2.0", "lodash": "^4.17.21", "lz-string": "^1.4.4", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-hast": "^13.0.0", - "moment": "^2.29.4", "normalize.css": "^8.0.1", "phaser": "^3.55.2", "query-string": "^9.0.0", "re-resizable": "^6.9.9", "react": "^18.3.1", - "react-ace": "^10.1.0", + "react-ace": "^12.0.0", "react-copy-to-clipboard": "^5.1.0", "react-debounce-render": "^8.0.2", "react-dom": "^18.3.1", "react-drag-drop-files": "^2.3.10", "react-draggable": "^4.4.5", "react-dropzone": "^14.2.3", - "react-i18next": "^14.1.0", + "react-i18next": "^15.0.0", "react-konva": "^18.2.10", "react-latex-next": "^3.0.0", "react-mde": "^11.5.0", @@ -94,7 +94,7 @@ "showdown": "^2.1.0", "sourceror": "^0.8.5", "unified": "^11.0.0", - "uuid": "^9.0.0", + "uuid": "^11.0.2", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz", "xml2js": "^0.6.0", "yareco": "^0.1.5" @@ -108,7 +108,7 @@ "@craco/craco": "^7.1.0", "@svgr/webpack": "^8.0.0", "@testing-library/jest-dom": "^6.0.0", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^15.0.6", "@testing-library/user-event": "^14.4.3", "@types/dompurify": "^3.0.5", "@types/estree": "^1.0.5", @@ -129,7 +129,6 @@ "@types/react-test-renderer": "^18.0.0", "@types/redux-mock-store": "^1.0.3", "@types/showdown": "^2.0.1", - "@types/uuid": "^9.0.0", "@types/xml2js": "^0.4.11", "babel-jest": "^29.7.0", "buffer": "^6.0.3", @@ -145,7 +144,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "https-browserify": "^1.0.0", "husky": "^9.0.0", - "npm-run-all2": "^6.0.0", + "npm-run-all2": "^7.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "prettier": "^3.3.3", diff --git a/renovate.json b/renovate.json index 99481ca534..efbc2b440b 100644 --- a/renovate.json +++ b/renovate.json @@ -1,13 +1,18 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", ":dependencyDashboard", "group:allNonMajor", ":label(dependencies)"], + "extends": [ + "config:recommended", + ":dependencyDashboard", + "group:allNonMajor", + ":label(dependencies)" + ], "major": { "dependencyDashboardApproval": true }, "packageRules": [ { - "matchPackagePatterns": ["*"], - "rangeStrategy": "update-lockfile" + "rangeStrategy": "update-lockfile", + "matchPackageNames": ["*"] } ], "enabledManagers": ["npm"] diff --git a/src/commons/achievement/utils/DateHelper.ts b/src/commons/achievement/utils/DateHelper.ts index 1d695f1a02..df7cbd696b 100644 --- a/src/commons/achievement/utils/DateHelper.ts +++ b/src/commons/achievement/utils/DateHelper.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import dayjs from 'dayjs'; const now = new Date(); @@ -38,12 +38,12 @@ export const timeFromExpired = (deadline?: Date) => export const prettifyDate = (deadline?: Date) => { if (deadline === undefined) return ''; - return moment(deadline).format('D MMMM YYYY HH:mm'); + return dayjs(deadline).format('D MMMM YYYY HH:mm'); }; export const prettifyTime = (time?: Date) => { if (time === undefined) return ''; - return moment(time).format('HH:mm'); + return dayjs(time).format('HH:mm'); }; // Converts Date to deadline countdown @@ -54,11 +54,11 @@ export const prettifyDeadline = (deadline?: Date) => { return 'Expired'; } - const now = moment(); + const now = dayjs(); - const weeksAway = Math.ceil(moment(deadline).diff(now, 'weeks', true)); - const daysAway = Math.ceil(moment(deadline).diff(now, 'days', true)); - const hoursAway = Math.ceil(moment(deadline).diff(now, 'hours', true)); + const weeksAway = Math.ceil(dayjs(deadline).diff(now, 'weeks', true)); + const daysAway = Math.ceil(dayjs(deadline).diff(now, 'days', true)); + const hoursAway = Math.ceil(dayjs(deadline).diff(now, 'hours', true)); let prettifiedDeadline = ''; if (weeksAway > 1) { diff --git a/src/commons/achievement/utils/InsertFakeAchievements.ts b/src/commons/achievement/utils/InsertFakeAchievements.ts index 9ba1c1d0d8..93182c3155 100644 --- a/src/commons/achievement/utils/InsertFakeAchievements.ts +++ b/src/commons/achievement/utils/InsertFakeAchievements.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import dayjs from 'dayjs'; import { cardBackgroundUrl, @@ -23,7 +23,7 @@ function insertFakeAchievements( inferencer: AchievementInferencer ) { const sortedOverviews = [...assessmentOverviews].sort((overview1, overview2) => - moment(overview1.closeAt).diff(moment(overview2.closeAt)) + dayjs(overview1.closeAt).diff(dayjs(overview2.closeAt)) ); const length = assessmentOverviews.length; diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index e0a54c99ac..b5e269220c 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -214,6 +214,10 @@ export function isSchemeLanguage(chapter: Chapter): boolean { ].includes(chapter); } +export function isCseVariant(variant: Variant): boolean { + return variant == Variant.EXPLICIT_CONTROL; +} + const pySubLanguages: Array> = [ { chapter: Chapter.PYTHON_1, variant: Variant.DEFAULT, displayName: 'Python \xa71' } //{ chapter: Chapter.PYTHON_2, variant: Variant.DEFAULT, displayName: 'Python \xa72' }, diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index bb5b9b79a8..adbe92f7de 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -191,9 +191,10 @@ const AssessmentWorkspace: React.FC = props => { }, []); useEffect(() => { - handleTeamOverviewFetch(props.assessmentId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (assessmentOverview && assessmentOverview.maxTeamSize > 1) { + handleTeamOverviewFetch(props.assessmentId); + } + }, [assessmentOverview, handleTeamOverviewFetch, props.assessmentId]); /** * After mounting (either an older copy of the assessment diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 7cca18b608..29f88df173 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -640,47 +640,22 @@ export const getGradingOverviews = async ( } const gradingOverviews = await resp.json(); - return { - count: gradingOverviews.count, - data: gradingOverviews.data.map((overview: any) => { - const gradingOverview: GradingOverview = { - assessmentId: overview.assessment.id, - assessmentNumber: overview.assessment.assessmentNumber, - assessmentName: overview.assessment.title, - assessmentType: overview.assessment.type, - studentId: overview.student ? overview.student.id : -1, - studentName: overview.student ? overview.student.name : undefined, - studentNames: overview.team - ? overview.team.team_members.map((member: { name: any }) => member.name) - : undefined, - studentUsername: overview.student ? overview.student.username : undefined, - studentUsernames: overview.team - ? overview.team.team_members.map((member: { username: any }) => member.username) - : undefined, - submissionId: overview.id, - submissionStatus: overview.status, - groupName: overview.student ? overview.student.groupName : '-', - groupLeaderId: overview.student ? overview.student.groupLeaderId : undefined, - isGradingPublished: overview.isGradingPublished, - progress: backendParamsToProgressStatus( - overview.assessment.isManuallyGraded, - overview.isGradingPublished, - overview.status, - overview.gradedCount, - overview.assessment.questionCount - ), - questionCount: overview.assessment.questionCount, - gradedCount: overview.gradedCount, - // XP - initialXp: overview.xp, - xpAdjustment: overview.xpAdjustment, - currentXp: overview.xp + overview.xpAdjustment, - maxXp: overview.assessment.maxXp, - xpBonus: overview.xpBonus - }; - return gradingOverview; - }) - }; + return respToGradingOverviews(gradingOverviews); +}; + +/* + * GET /courses/{courseId}/admin/grading/all_submissions + */ +export const getAllGradingOverviews = async (tokens: Tokens): Promise => { + const resp = await request(`${courseId()}/admin/grading/all_submissions`, 'GET', { + ...tokens + }); + if (!resp) { + return null; // invalid accessToken _and_ refreshToken + } + const gradingOverviews = await resp.json(); + + return respToGradingOverviews(gradingOverviews); }; /* @@ -1556,6 +1531,50 @@ export function* handleResponseError(resp: Response | null): any { yield call(showWarningMessage, respText); } +const respToGradingOverviews = (gradingOverviews: any): GradingOverviews => { + return { + count: gradingOverviews.count, + data: gradingOverviews.data.map((overview: any) => { + const gradingOverview: GradingOverview = { + assessmentId: overview.assessment.id, + assessmentNumber: overview.assessment.assessmentNumber, + assessmentName: overview.assessment.title, + assessmentType: overview.assessment.type, + studentId: overview.student ? overview.student.id : -1, + studentName: overview.student ? overview.student.name : undefined, + studentNames: overview.team + ? overview.team.team_members.map((member: { name: any }) => member.name) + : undefined, + studentUsername: overview.student ? overview.student.username : undefined, + studentUsernames: overview.team + ? overview.team.team_members.map((member: { username: any }) => member.username) + : undefined, + submissionId: overview.id, + submissionStatus: overview.status, + groupName: overview.student ? overview.student.groupName : '-', + groupLeaderId: overview.student ? overview.student.groupLeaderId : undefined, + isGradingPublished: overview.isGradingPublished, + progress: backendParamsToProgressStatus( + overview.assessment.isManuallyGraded, + overview.isGradingPublished, + overview.status, + overview.gradedCount, + overview.assessment.questionCount + ), + questionCount: overview.assessment.questionCount, + gradedCount: overview.gradedCount, + // XP + initialXp: overview.xp, + xpAdjustment: overview.xpAdjustment, + currentXp: overview.xp + overview.xpAdjustment, + maxXp: overview.assessment.maxXp, + xpBonus: overview.xpBonus + }; + return gradingOverview; + }) + }; +}; + const courseId: () => string = () => { const id = store.getState().session.courseId; if (id) { diff --git a/src/commons/utils/AceHelper.ts b/src/commons/utils/AceHelper.ts index 903824bdac..7e958f124c 100644 --- a/src/commons/utils/AceHelper.ts +++ b/src/commons/utils/AceHelper.ts @@ -31,6 +31,15 @@ export const selectMode = (chapter: Chapter, variant: Variant, library: string) ModeSelector(chapter, variant, library); }; +import 'ace-builds/src-noconflict/mode-c_cpp'; +import 'ace-builds/src-noconflict/mode-html'; +import 'ace-builds/src-noconflict/mode-java'; +import 'ace-builds/src-noconflict/mode-javascript'; +import 'ace-builds/src-noconflict/mode-python'; +import 'ace-builds/src-noconflict/mode-scheme'; +import 'ace-builds/src-noconflict/mode-typescript'; +import 'js-slang/dist/editors/ace/theme/source'; + export const getModeString = (chapter: Chapter, variant: Variant, library: string) => { // TODO: Create our own highlighting rules for the different sublanguages switch (chapter) { diff --git a/src/commons/utils/DateHelper.ts b/src/commons/utils/DateHelper.ts index 79454affba..ff40ad064f 100644 --- a/src/commons/utils/DateHelper.ts +++ b/src/commons/utils/DateHelper.ts @@ -1,4 +1,9 @@ -import moment from 'moment'; +import dayjs from 'dayjs'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +dayjs.extend(advancedFormat); +dayjs.extend(relativeTime); /** * Checks if a date is before or at the current time. @@ -9,8 +14,8 @@ import moment from 'moment'; * is before the time of execution of this function. */ export const beforeNow = (dateString: string): boolean => { - const date = moment(dateString); - const now = moment(); + const date = dayjs(dateString); + const now = dayjs(); return date.isBefore(now); }; @@ -25,25 +30,25 @@ export const beforeNow = (dateString: string): boolean => { * e.g 7th June, 20:09 */ export const getPrettyDate = (dateString: string): string => { - const date = moment(dateString); + const date = dayjs(dateString); const prettyDate = date.format('Do MMMM, HH:mm'); return prettyDate; }; export const getStandardDateTime = (dateString: string): string => { - const date = moment(dateString); + const date = dayjs(dateString); const prettyDate = date.format('MMMM Do YYYY, HH:mm'); return prettyDate; }; export const getStandardDate = (dateString: string): string => { - const date = moment(dateString); + const date = dayjs(dateString); const prettyDate = date.format('MMMM Do YYYY'); return prettyDate; }; export const getPrettyDateAfterHours = (dateString: string, hours: number): string => { - const date = moment(dateString).add(hours, 'hours'); + const date = dayjs(dateString).add(hours, 'hours'); const absolutePrettyDate = date.format('Do MMMM, HH:mm'); const relativePrettyDate = date.fromNow(); return `${absolutePrettyDate} (${relativePrettyDate})`; diff --git a/src/features/cseMachine/CseMachineAnimation.tsx b/src/features/cseMachine/CseMachineAnimation.tsx index 8c8cc70bd8..9249c7bca4 100644 --- a/src/features/cseMachine/CseMachineAnimation.tsx +++ b/src/features/cseMachine/CseMachineAnimation.tsx @@ -25,7 +25,8 @@ import { Frame } from './components/Frame'; import { ArrayValue } from './components/values/ArrayValue'; import CseMachine from './CseMachine'; import { Layout } from './CseMachineLayout'; -import { isBuiltInFn, isStreamFn } from './CseMachineUtils'; +import { isBuiltInFn, isInstr, isStreamFn } from './CseMachineUtils'; +import { isList, isSymbol } from './utils/scheme'; export class CseAnimation { static readonly animations: Animatable[] = []; @@ -153,7 +154,7 @@ export class CseAnimation { } if (isNode(lastControlItem)) { CseAnimation.handleNode(lastControlItem); - } else { + } else if (isInstr(lastControlItem)) { switch (lastControlItem.instrType) { case InstrType.APPLICATION: const appInstr = lastControlItem as AppInstr; @@ -283,6 +284,75 @@ export class CseAnimation { case InstrType.RESET: break; } + } else { + // these are either scheme lists or values. + // The value is a number, boolean, string or null. (control -> stash) + if ( + lastControlItem === null || + typeof lastControlItem === 'number' || + typeof lastControlItem === 'boolean' || + typeof lastControlItem === 'string' + ) { + CseAnimation.animations.push( + new ControlToStashAnimation(lastControlComponent, currStashComponent!) + ); + } + // The value is a symbol. (lookup, control -> stash) + else if (isSymbol(lastControlItem)) { + CseAnimation.animations.push( + new ControlToStashAnimation(lastControlComponent, currStashComponent!) + ); + } + // The value is a list. (control -> control) + else if (isList(lastControlItem)) { + // base our decision on the first element of the list. + const firstElement = (lastControlItem as any)[0]; + if (isSymbol(firstElement)) { + switch (firstElement.sym) { + case 'lambda': + case 'define': + case 'set!': + case 'if': + case 'begin': + CseAnimation.animations.push( + new ControlExpansionAnimation( + lastControlComponent, + CseAnimation.getNewControlItems() + ) + ); + break; + case 'quote': + CseAnimation.animations.push( + new ControlToStashAnimation(lastControlComponent, currStashComponent!) + ); + break; + case 'define-syntax': + // undefined was pushed onto the stash. + CseAnimation.animations.push( + new ControlToStashAnimation(lastControlComponent, currStashComponent!) + ); + break; + case 'syntax-rules': + // nothing. + default: + // it's probably an application, or a macro expansion. + // either way, it's a control -> control expansion. + CseAnimation.animations.push( + new ControlExpansionAnimation( + lastControlComponent, + CseAnimation.getNewControlItems() + ) + ); + break; + } + } else { + // it's probably an application. + CseAnimation.animations.push( + new ControlExpansionAnimation(lastControlComponent, CseAnimation.getNewControlItems()) + ); + } + } + return; } } diff --git a/src/features/cseMachine/CseMachineLayout.tsx b/src/features/cseMachine/CseMachineLayout.tsx index f9f37a6678..f5491b5bd9 100644 --- a/src/features/cseMachine/CseMachineLayout.tsx +++ b/src/features/cseMachine/CseMachineLayout.tsx @@ -11,6 +11,7 @@ import { ControlStack } from './components/ControlStack'; import { Level } from './components/Level'; import { StashStack } from './components/StashStack'; import { ArrayValue } from './components/values/ArrayValue'; +import { ContValue } from './components/values/ContValue'; import { FnValue } from './components/values/FnValue'; import { GlobalFnValue } from './components/values/GlobalFnValue'; import { PrimitiveValue } from './components/values/PrimitiveValue'; @@ -45,6 +46,7 @@ import { isUnassigned, setDifference } from './CseMachineUtils'; +import { Continuation, isContinuation, isSchemeNumber, isSymbol } from './utils/scheme'; /** this class encapsulates the logic for calculating the layout */ export class Layout { @@ -372,6 +374,8 @@ export class Layout { return new UnassignedValue(reference); } else if (isPrimitiveData(data)) { return new PrimitiveValue(data, reference); + } else if (isSymbol(data) || isSchemeNumber(data)) { + return new PrimitiveValue(data, reference); } else { const existingValue = Layout.values.get( isBuiltInFn(data) || isStreamFn(data) ? data : data.id @@ -383,6 +387,8 @@ export class Layout { if (isDataArray(data)) { return new ArrayValue(data, reference); + } else if (isContinuation(data)) { + return new ContValue(data, reference); } else if (isGlobalFn(data)) { assert(reference instanceof Binding); return new GlobalFnValue(data, reference); @@ -394,9 +400,12 @@ export class Layout { } } - static memoizeValue(data: GlobalFn | NonGlobalFn | StreamFn | DataArray, value: Value) { + static memoizeValue( + data: GlobalFn | NonGlobalFn | StreamFn | Continuation | DataArray, + value: Value + ) { if (isBuiltInFn(data) || isStreamFn(data)) Layout.values.set(data, value); - else Layout.values.set(data.id, value); + else Layout.values.set((data as any).id, value); } /** diff --git a/src/features/cseMachine/CseMachineTypes.ts b/src/features/cseMachine/CseMachineTypes.ts index a2e9445c43..5346e4347d 100644 --- a/src/features/cseMachine/CseMachineTypes.ts +++ b/src/features/cseMachine/CseMachineTypes.ts @@ -1,3 +1,5 @@ +import { _Symbol } from 'js-slang/dist/alt-langs/scheme/scm-slang/src/stdlib/base'; +import { SchemeNumber } from 'js-slang/dist/alt-langs/scheme/scm-slang/src/stdlib/core'; import { EnvTree as EnvironmentTree, EnvTreeNode as EnvironmentTreeNode @@ -11,6 +13,7 @@ import { ArrayUnit } from './components/ArrayUnit'; import { Binding } from './components/Binding'; import { Frame } from './components/Frame'; import { Level } from './components/Level'; +import { Continuation } from './utils/scheme'; /** this interface defines a drawing function */ export interface Drawable { @@ -84,7 +87,15 @@ 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 + | NonGlobalFn + | GlobalFn + | Unassigned + | DataArray + | SchemeNumber + | _Symbol + | Continuation; /** 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 9087404465..eb57b673c1 100644 --- a/src/features/cseMachine/CseMachineUtils.ts +++ b/src/features/cseMachine/CseMachineUtils.ts @@ -25,9 +25,11 @@ import classes from 'src/styles/Draggable.module.scss'; import { ArrayUnit } from './components/ArrayUnit'; import { Binding } from './components/Binding'; import { ControlItemComponent } from './components/ControlItemComponent'; +import { isNode } from './components/ControlStack'; import { Frame } from './components/Frame'; import { StashItemComponent } from './components/StashItemComponent'; import { ArrayValue } from './components/values/ArrayValue'; +import { ContValue } from './components/values/ContValue'; import { FnValue } from './components/values/FnValue'; import { GlobalFnValue } from './components/values/GlobalFnValue'; import { Value } from './components/values/Value'; @@ -57,6 +59,7 @@ import { isCustomPrimitive, needsNewRepresentation } from './utils/altLangs'; +import { isContinuation, schemeToString } from './utils/scheme'; class AssertionError extends Error { constructor(msg?: string) { super(msg); @@ -233,6 +236,12 @@ export function setDifference(set1: Set, set2: Set) { * always prioritised over array units. */ export function isMainReference(value: Value, reference: ReferenceType) { + if (isContinuation(value.data)) { + return ( + reference instanceof Binding && + isEnvEqual(reference.frame.environment, value.data.getEnv()[0]) + ); + } if (isGlobalFn(value.data)) { return ( reference instanceof Binding && @@ -583,6 +592,19 @@ export function getControlItemComponent( ? index === Math.min(Layout.control.size() - 1, 9) : index === Layout.control.size() - 1; if (!isInstr(controlItem)) { + if (!isNode(controlItem)) { + // at the moment, the only non-node and non-instruction control items are + // literals from scheme. + const representation = schemeToString(controlItem as any); + return new ControlItemComponent( + representation, + representation, + stackHeight, + highlightOnHover, + unhighlightOnHover, + topItem + ); + } // there's no reason to provide an alternate representation // for a instruction. if (needsNewRepresentation(chapter)) { @@ -609,11 +631,13 @@ export function getControlItemComponent( topItem ); } - switch (controlItem.type) { + + // at this point, the control item is a node. + switch ((controlItem as any).type) { case 'Program': // If the control item is the whole program // add {} to represent the implicit block - const originalText = astToString(controlItem) + const originalText = astToString(controlItem as any) .trim() .split('\n') .map(line => `\t\t${line}`) @@ -629,7 +653,9 @@ export function getControlItemComponent( ); case 'Literal': const textL = - typeof controlItem.value === 'string' ? `"${controlItem.value}"` : controlItem.value; + typeof (controlItem as any).value === 'string' + ? `"${(controlItem as any).value}"` + : (controlItem as any).value; return new ControlItemComponent( textL, String(textL), @@ -639,7 +665,7 @@ export function getControlItemComponent( topItem ); default: - const text = astToString(controlItem).trim(); + const text = astToString(controlItem as any).trim(); return new ControlItemComponent( text, text, @@ -846,10 +872,10 @@ export function getStashItemComponent( index: number, _chapter: Chapter ): StashItemComponent { - 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; + let arrowTo: ArrayValue | FnValue | GlobalFnValue | ContValue | undefined; + if (isFunction(stashItem) || isDataArray(stashItem || isContinuation(stashItem))) { + if (isClosure(stashItem) || isDataArray(stashItem) || isContinuation(stashItem)) { + arrowTo = Layout.values.get(stashItem.id) as ArrayValue | FnValue | ContValue; } else { arrowTo = Layout.values.get(stashItem) as FnValue | GlobalFnValue; } diff --git a/src/features/cseMachine/components/Binding.tsx b/src/features/cseMachine/components/Binding.tsx index 187f020e08..e2abdeb8d5 100644 --- a/src/features/cseMachine/components/Binding.tsx +++ b/src/features/cseMachine/components/Binding.tsx @@ -9,6 +9,7 @@ import { GenericArrow } from './arrows/GenericArrow'; import { Frame } from './Frame'; import { Text } from './Text'; import { ArrayValue } from './values/ArrayValue'; +import { ContValue } from './values/ContValue'; import { FnValue } from './values/FnValue'; import { GlobalFnValue } from './values/GlobalFnValue'; import { PrimitiveValue } from './values/PrimitiveValue'; @@ -68,7 +69,9 @@ export class Binding extends Visible { ? this.value.x() + this.value.width() - this.x() + - (this.value instanceof FnValue || this.value instanceof GlobalFnValue + (this.value instanceof FnValue || + this.value instanceof GlobalFnValue || + this.value instanceof ContValue ? this.value.tooltipWidth : 0) : this.key.width(); diff --git a/src/features/cseMachine/components/ControlStack.tsx b/src/features/cseMachine/components/ControlStack.tsx index 6f0850380c..01770ce3d1 100644 --- a/src/features/cseMachine/components/ControlStack.tsx +++ b/src/features/cseMachine/components/ControlStack.tsx @@ -43,14 +43,18 @@ export class ControlStack extends Visible implements IHoverable { // Function to convert the stack items to their components let i = 0; const controlItemToComponent = (controlItem: ControlItem) => { - const node = isNode(controlItem) ? controlItem : controlItem.srcNode; + const node = isNode(controlItem) + ? controlItem + : isInstr(controlItem) + ? controlItem.srcNode + : controlItem; let highlightOnHover = () => {}; let unhighlightOnHover = () => {}; highlightOnHover = () => { - if (node.loc) { - const start = node.loc.start.line - 1; - const end = node.loc.end.line - 1; + if ((node as any).loc !== undefined) { + const start = (node as any).loc.start.line - 1; + const end = (node as any).loc.end.line - 1; CseMachine.setEditorHighlightedLines([[start, end]]); } }; diff --git a/src/features/cseMachine/components/Frame.tsx b/src/features/cseMachine/components/Frame.tsx index 36e49344ad..e632fd19f6 100644 --- a/src/features/cseMachine/components/Frame.tsx +++ b/src/features/cseMachine/components/Frame.tsx @@ -16,6 +16,7 @@ import { isPrimitiveData, isUnassigned } from '../CseMachineUtils'; +import { isContinuation } from '../utils/scheme'; import { ArrowFromFrame } from './arrows/ArrowFromFrame'; import { GenericArrow } from './arrows/GenericArrow'; import { Binding } from './Binding'; @@ -124,7 +125,7 @@ export class Frame extends Visible implements IHoverable { const value = unreferencedValues[i]; if (isDataArray(value)) { for (const data of value) { - if ((isDataArray(data) && data !== value) || isClosure(data)) { + if ((isDataArray(data) && data !== value) || isClosure(data) || isContinuation(data)) { const prev = unreferencedValues.findIndex(value => value.id === data.id); if (prev > -1) { unreferencedValues.splice(prev, 1); diff --git a/src/features/cseMachine/components/StashItemComponent.tsx b/src/features/cseMachine/components/StashItemComponent.tsx index 1bb694a6a9..b0d37e634c 100644 --- a/src/features/cseMachine/components/StashItemComponent.tsx +++ b/src/features/cseMachine/components/StashItemComponent.tsx @@ -24,8 +24,10 @@ import { setUnhoveredStyle, truncateText } from '../CseMachineUtils'; +import { isContinuation } from '../utils/scheme'; import { ArrowFromStashItemComponent } from './arrows/ArrowFromStashItemComponent'; import { ArrayValue } from './values/ArrayValue'; +import { ContValue } from './values/ContValue'; import { Visible } from './Visible'; export class StashItemComponent extends Visible implements IHoverable { @@ -42,21 +44,23 @@ export class StashItemComponent extends Visible implements IHoverable { stackWidth: number, /** The index number of this stack item */ readonly index: number, - arrowTo?: FnValue | GlobalFnValue | ArrayValue + arrowTo?: FnValue | GlobalFnValue | ContValue | ArrayValue ) { super(); const valToStashRep = (val: any): string => { return typeof val === 'string' ? `'${val}'`.trim() - : isNonGlobalFn(val) - ? 'closure' - : isDataArray(val) - ? arrowTo - ? 'pair/array' - : JSON.stringify(val) - : isSourceObject(val) - ? val.toReplString() - : String(value); + : isContinuation(val) + ? 'continuation' + : isNonGlobalFn(val) + ? 'closure' + : isDataArray(val) + ? arrowTo + ? 'pair/array' + : JSON.stringify(val) + : isSourceObject(val) + ? val.toReplString() + : String(value); }; this.text = truncateText( valToStashRep(value), diff --git a/src/features/cseMachine/components/arrows/Arrow.tsx b/src/features/cseMachine/components/arrows/Arrow.tsx index 2feebcabcd..195162010c 100644 --- a/src/features/cseMachine/components/arrows/Arrow.tsx +++ b/src/features/cseMachine/components/arrows/Arrow.tsx @@ -6,6 +6,7 @@ import { ControlItemComponent } from '../ControlItemComponent'; import { Frame } from '../Frame'; import { StashItemComponent } from '../StashItemComponent'; import { Text } from '../Text'; +import { ContValue } from '../values/ContValue'; import { FnValue } from '../values/FnValue'; import { GlobalFnValue } from '../values/GlobalFnValue'; import { Visible } from '../Visible'; @@ -36,7 +37,7 @@ export abstract class Arrow { /** factory method that returns the corresponding arrow depending on where the arrow is `from` */ public static from(source: Visible): GenericArrow { if (source instanceof Frame) return new ArrowFromFrame(source); - if (source instanceof FnValue || source instanceof GlobalFnValue) + if (source instanceof FnValue || source instanceof GlobalFnValue || source instanceof ContValue) return new ArrowFromFn(source); if (source instanceof Text) return new ArrowFromText(source); if (source instanceof ArrayUnit) return new ArrowFromArrayUnit(source); diff --git a/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx b/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx index b670f06e4a..83492003d2 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromArrayUnit.tsx @@ -2,6 +2,7 @@ import { Config } from '../../CseMachineConfig'; import { StepsArray } from '../../CseMachineTypes'; import { ArrayUnit } from '../ArrayUnit'; import { ArrayValue } from '../values/ArrayValue'; +import { ContValue } from '../values/ContValue'; import { FnValue } from '../values/FnValue'; import { GlobalFnValue } from '../values/GlobalFnValue'; import { Value } from '../values/Value'; @@ -23,7 +24,7 @@ export class ArrowFromArrayUnit extends GenericArrow { (x, y) => [x + Config.DataUnitWidth / 2, y + Config.DataUnitHeight / 2] ]; - if (to instanceof FnValue || to instanceof GlobalFnValue) { + if (to instanceof FnValue || to instanceof GlobalFnValue || to instanceof ContValue) { steps.push(() => [from.x() < to.x() ? to.x() : to.centerX, to.y()]); } else if (to instanceof ArrayValue) { if (from.y() === to.y()) { diff --git a/src/features/cseMachine/components/arrows/ArrowFromControlItemComponent.tsx b/src/features/cseMachine/components/arrows/ArrowFromControlItemComponent.tsx index 756a8a775d..e29f0904f4 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromControlItemComponent.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromControlItemComponent.tsx @@ -4,12 +4,13 @@ import { ControlStashConfig } from '../../CseMachineControlStashConfig'; import { StepsArray } from '../../CseMachineTypes'; import { ControlItemComponent } from '../ControlItemComponent'; import { Frame } from '../Frame'; +import { ContValue } from '../values/ContValue'; import { GenericArrow } from './GenericArrow'; /** this class encapsulates an GenericArrow to be drawn between 2 points */ export class ArrowFromControlItemComponent extends GenericArrow< ControlItemComponent, - Frame | FnValue | GlobalFnValue + Frame | FnValue | GlobalFnValue | ContValue > { protected calculateSteps() { const from = this.source; diff --git a/src/features/cseMachine/components/arrows/ArrowFromFn.tsx b/src/features/cseMachine/components/arrows/ArrowFromFn.tsx index 37b4585cf3..d23701ffd6 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromFn.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromFn.tsx @@ -1,13 +1,14 @@ import { Config } from '../../CseMachineConfig'; import { StepsArray } from '../../CseMachineTypes'; import { Frame } from '../Frame'; +import { ContValue } from '../values/ContValue'; import { FnValue } from '../values/FnValue'; import { GlobalFnValue } from '../values/GlobalFnValue'; import { GenericArrow } from './GenericArrow'; /** this class encapsulates an GenericArrow to be drawn between 2 points */ -export class ArrowFromFn extends GenericArrow { - constructor(from: FnValue | GlobalFnValue) { +export class ArrowFromFn extends GenericArrow { + constructor(from: FnValue | GlobalFnValue | ContValue) { super(from); this.faded = !from.isReferenced(); } @@ -18,8 +19,14 @@ export class ArrowFromFn extends GenericArrow { if (!to) return []; const steps: StepsArray = [ - (x, y) => [x + Config.FnRadius * 3, y], - (x, y) => [x, y - Config.FnRadius * 2], + (x, y) => + this.source instanceof ContValue + ? [x + Config.FnRadius * 2, y] + : [x + Config.FnRadius * 3, y], + (x, y) => + this.source instanceof ContValue + ? [x, to.y() + Config.FnRadius] + : [x, y - Config.FnRadius * 2], (x, y) => [to.x() + (from.x() < to.x() ? 0 : to.width()), y] ]; diff --git a/src/features/cseMachine/components/arrows/ArrowFromStashItemComponent.tsx b/src/features/cseMachine/components/arrows/ArrowFromStashItemComponent.tsx index cc7971246c..6e22b94b66 100644 --- a/src/features/cseMachine/components/arrows/ArrowFromStashItemComponent.tsx +++ b/src/features/cseMachine/components/arrows/ArrowFromStashItemComponent.tsx @@ -4,12 +4,13 @@ import { StepsArray } from '../../CseMachineTypes'; import { Frame } from '../Frame'; import { StashItemComponent } from '../StashItemComponent'; import { ArrayValue } from '../values/ArrayValue'; +import { ContValue } from '../values/ContValue'; import { GenericArrow } from './GenericArrow'; /** this class encapsulates an GenericArrow to be drawn between 2 points */ export class ArrowFromStashItemComponent extends GenericArrow< StashItemComponent, - Frame | FnValue | GlobalFnValue | ArrayValue + Frame | FnValue | GlobalFnValue | ArrayValue | ContValue > { protected calculateSteps() { const from = this.source; diff --git a/src/features/cseMachine/components/values/ContValue.tsx b/src/features/cseMachine/components/values/ContValue.tsx new file mode 100644 index 0000000000..0e54531631 --- /dev/null +++ b/src/features/cseMachine/components/values/ContValue.tsx @@ -0,0 +1,187 @@ +import { Control, Stash } from 'js-slang/dist/cse-machine/interpreter'; +import { Environment } from 'js-slang/dist/types'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { Label } from 'konva/lib/shapes/Label'; +import React, { RefObject } from 'react'; +import { + Circle, + Group, + Label as KonvaLabel, + Rect, + Tag as KonvaTag, + Text as KonvaText +} from 'react-konva'; + +import CseMachine from '../../CseMachine'; +import { Config, ShapeDefaultProps } from '../../CseMachineConfig'; +import { Layout } from '../../CseMachineLayout'; +import { IHoverable, ReferenceType } from '../../CseMachineTypes'; +import { + defaultStrokeColor, + defaultTextColor, + fadedStrokeColor, + fadedTextColor, + getTextWidth, + isMainReference, + setHoveredCursor, + setUnhoveredCursor +} from '../../CseMachineUtils'; +import { Continuation } from '../../utils/scheme'; +import { ArrowFromFn } from '../arrows/ArrowFromFn'; +import { Binding } from '../Binding'; +import { Frame } from '../Frame'; +import { Value } from './Value'; + +/** this class encapsulates a Scheme Continuation that + * contains extra props such as environment, control and stash */ +export class ContValue extends Value implements IHoverable { + readonly radius: number = Config.FnRadius; + readonly innerRadius: number = Config.FnInnerRadius; + readonly labelRef: RefObject