Add support for self-modifying programs to core

This commit is contained in:
Nilay Majorwar 2022-01-26 20:01:53 +05:30
parent 45385a3266
commit 7b7475a4fb
4 changed files with 94 additions and 10 deletions

View File

@ -14,6 +14,14 @@ export type DocumentRange = {
charRange?: CharRange;
};
/** Type denoting a document edit */
export type DocumentEdit = {
/** Range to replace with the given text. Keep empty to insert text */
range: DocumentRange;
/** Text to replace the given range with */
text: string;
};
/** Source code token provider for the language, specific to Monaco */
export type MonacoTokensProvider = monaco.languages.IMonarchLanguage;
@ -30,6 +38,9 @@ export type StepExecutionResult<RS> = {
/** String to write to program output */
output?: string;
/** Self-modifying programs: edit to apply on code */
codeEdits?: DocumentEdit[];
/**
* Used to highlight next line to be executed in the editor.
* Passing `null` indicates reaching the end of program.

View File

@ -44,6 +44,15 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
rendererRef.current!.updateState(result.rendererState);
codeEditorRef.current!.updateHighlights(result.nextStepLocation);
outputEditorRef.current!.append(result.output);
// Self-modifying programs: update code
if (result.codeEdits != null)
codeEditorRef.current!.editCode(result.codeEdits);
// End of program: reset code to original version
if (!result.nextStepLocation) codeEditorRef.current!.endExecutionMode();
// RuntimeError: print error to output
if (error) outputEditorRef.current!.setError(error);
};
@ -60,13 +69,16 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
outputEditorRef.current!.reset();
await execController.resetState();
const error = await execController.prepare(
codeEditorRef.current!.getValue(),
codeEditorRef.current!.getCode(),
inputEditorRef.current!.getValue()
);
// Check for ParseError, else begin execution
if (error) outputEditorRef.current!.setError(error);
else await execController.execute(updateWithResult, execInterval);
else {
codeEditorRef.current!.startExecutionMode();
await execController.execute(updateWithResult, execInterval);
}
};
/** Pause the ongoing execution */
@ -120,6 +132,7 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
await execController.resetState();
rendererRef.current!.updateState(null);
codeEditorRef.current!.updateHighlights(null);
codeEditorRef.current!.endExecutionMode();
};
/** Translate execution controller state to debug controls state */
@ -137,7 +150,6 @@ export const Mainframe = <RS extends {}>({ langName, provider }: Props<RS>) => {
<CodeEditor
ref={codeEditorRef}
languageId={langName}
readOnly={execController.state === "processing"}
defaultValue={providerRef.current.sampleProgram}
tokensProvider={providerRef.current.editorTokensProvider}
onValidateCode={execController.validateCode}

View File

@ -1,9 +1,14 @@
import React from "react";
import Editor from "@monaco-editor/react";
import { useEditorLanguageConfig } from "./use-editor-lang-config";
import { DocumentRange, MonacoTokensProvider } from "../../engines/types";
import {
DocumentEdit,
DocumentRange,
MonacoTokensProvider,
} from "../../engines/types";
import {
createHighlightRange,
createMonacoDocEdit,
EditorInstance,
MonacoInstance,
} from "./monaco-utils";
@ -14,12 +19,26 @@ import { useDarkMode } from "../providers/dark-mode-provider";
import { WorkerParseError } from "../../engines/worker-errors";
import { useCodeValidator } from "./use-code-validator";
/** Keeps track of user's original program and modifications done to it */
type ChangeTrackerValue = {
/** The user's original program code */
original: string;
/** Tracks if code was modified during execution */
changed: boolean;
};
// Interface for interacting with the editor
export interface CodeEditorRef {
/** Get the current text content of the editor */
getValue: () => string;
getCode: () => string;
/** Update value of code */
editCode: (edits: DocumentEdit[]) => void;
/** Update code highlights */
updateHighlights: (highlights: DocumentRange | null) => void;
/** Start execution mode - readonly editor with modifyable contents */
startExecutionMode: () => void;
/** End execution mode - reset contents and readonly state */
endExecutionMode: () => void;
}
type Props = {
@ -29,8 +48,6 @@ type Props = {
defaultValue: string;
/** Tokens provider for the language */
tokensProvider?: MonacoTokensProvider;
/** Set editor as read-only */
readOnly?: boolean;
/** Callback to validate code syntax */
onValidateCode: (code: string) => Promise<WorkerParseError | undefined>;
/** Callback to update debugging breakpoints */
@ -44,9 +61,16 @@ type Props = {
const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
const [editor, setEditor] = React.useState<EditorInstance | null>(null);
const [monaco, setMonaco] = React.useState<MonacoInstance | null>(null);
const [readOnly, setReadOnly] = React.useState(false);
const highlightRange = React.useRef<string[]>([]);
const { isDark } = useDarkMode();
// Code modification tracker, used in execution mode
const changeTracker = React.useRef<ChangeTrackerValue>({
original: "",
changed: false,
});
// Breakpoints
useEditorBreakpoints({
editor,
@ -89,8 +113,25 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
React.useImperativeHandle(
ref,
() => ({
getValue: () => editor!.getValue(),
getCode: () => editor!.getValue(),
editCode: (edits) => {
changeTracker.current.changed = true;
const monacoEdits = edits.map(createMonacoDocEdit);
editor!.getModel()!.applyEdits(monacoEdits);
},
updateHighlights,
startExecutionMode: () => {
changeTracker.current.original = editor!.getValue();
changeTracker.current.changed = false;
setReadOnly(true);
},
endExecutionMode: () => {
setReadOnly(false);
if (changeTracker.current.changed) {
editor!.getModel()!.setValue(changeTracker.current.original);
changeTracker.current.changed = false;
}
},
}),
[editor]
);
@ -112,7 +153,7 @@ const CodeEditorComponent = (props: Props, ref: React.Ref<CodeEditorRef>) => {
options={{
minimap: { enabled: false },
glyphMargin: true,
readOnly: props.readOnly,
readOnly: readOnly,
}}
/>
);

View File

@ -1,5 +1,5 @@
import monaco from "monaco-editor";
import { DocumentRange } from "../../engines/types";
import { DocumentEdit, DocumentRange } from "../../engines/types";
import { WorkerParseError } from "../../engines/worker-errors";
/** Type alias for an instance of Monaco editor */
@ -60,6 +60,26 @@ export const createValidationMarker = (
};
};
/**
* Convert a DocumentEdit instance to Monaco edit object format.
* @param edit DocumentEdit to convert to Monaco format
* @returns Instance of Monaco's edit object
*/
export const createMonacoDocEdit = (
edit: DocumentEdit
): monaco.editor.IIdentifiedSingleEditOperation => {
const location = get1IndexedLocation(edit.range);
return {
text: edit.text,
range: {
startLineNumber: location.line,
endLineNumber: location.line,
startColumn: location.charRange?.start || 0,
endColumn: location.charRange?.end || 1000,
},
};
};
/**
* Convert a DocumentRange to use 1-indexed values. Used since language engines
* use 0-indexed ranges but Monaco requires 1-indexed ranges.