From 8fef11f172c0fbf4029eab2fb0f7f9a3d1491a61 Mon Sep 17 00:00:00 2001 From: "DESKTOP-G08HS3B\\Lee Yi" Date: Mon, 3 Mar 2025 02:07:43 -0500 Subject: [PATCH 01/35] Relocate svmc repl prompt --- README.md | 1 - docs/lib/concurrency.js | 32 - docs/md/README_3_CONCURRENT.md | 62 - docs/md/README_CONCURRENCY.md | 10 - docs/md/README_top.md | 4 - docs/specs/Makefile | 2 +- docs/specs/source_3_concurrent.tex | 106 - docs/specs/source_concurrency.tex | 9 - scripts/docs.mjs | 21 - src/constants.ts | 1 - src/createContext.ts | 2 - src/editors/ace/docTooltip/index.ts | 2 - src/index.ts | 9 +- .../preprocessor/__tests__/preprocessor.ts | 2 +- .../transformers/hoistAndMergeImports.ts | 2 +- .../__tests__/transformers/removeExports.ts | 2 +- .../transformProgramToFunctionDeclaration.ts | 2 +- src/repl/__tests__/main.ts | 20 + src/repl/__tests__/svmc.ts | 110 + src/repl/__tests__/transpiler.ts | 66 + src/repl/__tests__/utils.ts | 38 + src/repl/index.ts | 13 +- src/repl/main.ts | 11 + src/repl/svmc.ts | 106 + src/repl/transpiler.ts | 108 +- src/runner/sourceRunner.ts | 56 +- src/schedulers.ts | 105 - src/stdlib/inspector.ts | 22 +- src/transpiler/__tests__/transpiled-code.ts | 2 +- src/types.ts | 19 +- src/utils/{testing.ts => testing/index.ts} | 16 +- src/utils/testing/misc.ts | 27 + src/utils/{ast => testing}/sanitizer.ts | 0 src/vm/__tests__/svml-machine.ts | 1577 -------------- src/vm/svmc.ts | 231 -- src/vm/svml-machine.ts | 1872 ----------------- 36 files changed, 463 insertions(+), 4205 deletions(-) delete mode 100644 docs/lib/concurrency.js delete mode 100644 docs/md/README_3_CONCURRENT.md delete mode 100644 docs/md/README_CONCURRENCY.md delete mode 100644 docs/specs/source_3_concurrent.tex delete mode 100644 docs/specs/source_concurrency.tex create mode 100644 src/repl/__tests__/main.ts create mode 100644 src/repl/__tests__/svmc.ts create mode 100644 src/repl/__tests__/transpiler.ts create mode 100644 src/repl/__tests__/utils.ts create mode 100644 src/repl/main.ts create mode 100644 src/repl/svmc.ts delete mode 100644 src/schedulers.ts rename src/utils/{testing.ts => testing/index.ts} (96%) create mode 100644 src/utils/testing/misc.ts rename src/utils/{ast => testing}/sanitizer.ts (100%) delete mode 100644 src/vm/__tests__/svml-machine.ts delete mode 100644 src/vm/svmc.ts delete mode 100644 src/vm/svml-machine.ts diff --git a/README.md b/README.md index 7121fd7d7..55c79f542 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ Currently, valid CHAPTER/VARIANT combinations are: - `--chapter=2 --variant=interpreter` - `--chapter=2 --variant=typed` - `--chapter=3 --variant=default` -- `--chapter=3 --variant=concurrent` - `--chapter=3 --variant=interpreter` - `--chapter=3 --variant=typed` - `--chapter=4 --variant=default` diff --git a/docs/lib/concurrency.js b/docs/lib/concurrency.js deleted file mode 100644 index 75fa7c269..000000000 --- a/docs/lib/concurrency.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Setup multiple threads for concurrent execution. For each - * function f_i, - * setup a thread t_i that executes the body of - * f_i. Any parameters of f_i refer - * to undefined during execution. - * The thread that called concurrent_execute - * runs concurrently with all t_i. Returns - * undefined. This is an atomic operation. - * @param {function} f_1,f_2,...,f_n - given functions - * @returns {undefined} undefined - */ -function concurrent_execute() {} - -/** - * Assumes the head of pair p is a boolean - * b. Sets the head of p to - * true. Returns b. This is an - * atomic operation. - * @param {array} p - given pair - * @returns {value} - head of pair b - */ -function test_and_set(p) {} - -/** - * Sets the head of pair p to - * false. Returns undefined. - * This is an atomic operation. - * @param {array} p - given pair - * @returns {undefined} undefined - */ -function clear(p) {} diff --git a/docs/md/README_3_CONCURRENT.md b/docs/md/README_3_CONCURRENT.md deleted file mode 100644 index c4147ab00..000000000 --- a/docs/md/README_3_CONCURRENT.md +++ /dev/null @@ -1,62 +0,0 @@ -Source §3 Concurrent is a small programming language, designed for the third chapter -of the textbook -Structure and Interpretation -of Computer Programs, JavaScript Adaptation (SICP JS). - -## What names are predeclared in Source §3 Concurrent? - -On the right, you see all predeclared names of Source §3 Concurrent, in alphabetical -order. Click on a name to see how it is defined and used. They come in these groups: - - -## What can you do in Source §3 Concurrent? - -You can use all features of -Source §3 and all -features that are introduced in -chapter 3.4 of the -textbook. -Below are the features that Source §3 Concurrent adds to Source §3. - -### Concurrency - -To introduce concurrency into your programs, you can use the -functions in the CONCURRENCY library. The program -runs concurrently with the threads that it creates. The program terminates when -all threads terminate. Any result value from any of the threads, including the -program's thread, are ignored. Use the predeclared `display` function to display -result values. - -## You want the definitive specs? - -For our development team, we are maintaining a definitive description -of the language, called the -Specification of Source §3 Concurrent. Feel free to -take a peek! - - diff --git a/docs/md/README_CONCURRENCY.md b/docs/md/README_CONCURRENCY.md deleted file mode 100644 index e3a1f28e7..000000000 --- a/docs/md/README_CONCURRENCY.md +++ /dev/null @@ -1,10 +0,0 @@ -CONCURRENCY provides three functions for introducing concurrency. -Click on a name on the right to see how they are defined and used. - -Concurrency is covered in -the textbook -Structure and Interpretation -of Computer Programs, JavaScript Adaptation (SICP JS) -in -section 3.4.2 Mechanisms for Controlling Concurrency. - diff --git a/docs/md/README_top.md b/docs/md/README_top.md index 33aedac01..17e62e591 100644 --- a/docs/md/README_top.md +++ b/docs/md/README_top.md @@ -31,8 +31,6 @@ the members of our learning community. #### Source §2 Typed -#### Source §3 Concurrent - #### Source §3 Typed #### Source §4 Typed @@ -58,8 +56,6 @@ the Source Academy. #### Specification of Source §2 Typed -#### Specification of Source §3 Concurrent - #### Specification of Source §3 Typed #### Specification of Source §4 Typed diff --git a/docs/specs/Makefile b/docs/specs/Makefile index 9c90edd42..30cf44087 100644 --- a/docs/specs/Makefile +++ b/docs/specs/Makefile @@ -1,6 +1,6 @@ PDFLATEX = latexmk -pdf -SPECSNUMS = 1 1_wasm 1_type_inference 1_infinite_loop_detection 1_typed 2 2_typed 3_type_inference 3 3_concurrent 3_typed 4 4_explicitcontrol 4_typed styleguide 2_stepper studio_2 python_1 +SPECSNUMS = 1 1_wasm 1_type_inference 1_infinite_loop_detection 1_typed 2 2_typed 3_type_inference 3 3_typed 4 4_explicitcontrol 4_typed styleguide 2_stepper studio_2 python_1 SPECS = $(SPECSNUMS:%=source_%) diff --git a/docs/specs/source_3_concurrent.tex b/docs/specs/source_3_concurrent.tex deleted file mode 100644 index 6631becee..000000000 --- a/docs/specs/source_3_concurrent.tex +++ /dev/null @@ -1,106 +0,0 @@ -\input source_header.tex - -\begin{document} - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - \docheader{2021}{Source}{\S 3 Concurrent}{Jonathan Chan, Martin Henz, Koo Zhengqun} - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -\input source_intro.tex - -Source \S 3 Concurrent is a concurrent extension of Source \S 3. - -\section{Changes} - -Source \S 3 Concurrent modifies Source \S 3 in the following ways: -\begin{itemize} -\item Concurrency support functions are added, see Section~\textbf{Concurrency Support} on page \pageref{conc_supp}. -\item The given program starts in a thread that runs concurrently with any - threads that are created during the execution of the program. -\item Neither the thread of the give program nor any other threads produce - any values as results. Their effect is observable through \emph{side effects} - such as calls of the \lstinline{display} primitive. - \item Import directives are currently not supported. -\end{itemize} -\noindent -The concurrency of Source \S 3 Concurrent is thread-based and deviates -from the event-driven concurrency of -\href{http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf}{\color{DarkBlue} - ECMAScript 2018 ($9^{\textrm{th}}$ Edition)}. Source \S 3 Concurrent is -motivated by Section 3.4 of the textbook -\href{https://sourceacademy.org/sicpjs}{\color{DarkBlue}\emph{Structure and Interpretation -of Computer Programs}, JavaScript Adaptation}. - -\section{Concurrency} - -We specify \emph{interleaving semantics} for Source \S 3 Concurrent. -The effect of executing a Source \S 3 Concurrent program -should be explainable as a single sequence of atomic actions. Each thread -specifies a particular sequence of actions in a specific order, -and the implementation is free to interleave the sequences of the threads -into a single sequence, as long as the following conditions are met: -\begin{enumerate} -\item The order of actions within each thread is respected (sequential - threads). -\item Any action that is included in any thread's sequence of actions will - eventually be executed (no starvation). -\end{enumerate} -\noindent -The atomic actions are primitive steps such as accessing the value of a name, -accessing a data structure, reducing a conditional expression or statement, -carrying out a primitive operation or calling a function. Such atomic actions -are considered \emph{uninterruptible}; they specify the \emph{granularity} -of the concurrency. - -\input source_bnf.tex - -\newpage - -\input source_3_bnf_without_import.tex - -\newpage - -\input source_return - -\input source_boolean_operators - -\input source_loops - -\input source_names_lang - -\input source_numbers - -\input source_strings - -\input source_arrays - -\input source_comments - -\input source_typing_3 - -\input source_standard - -\input source_misc - -\input source_math - -\input source_concurrency - -\input source_lists - -\input source_pair_mutators - -\input source_array_support - -\input source_streams - -\input source_js_differences - -\newpage - -\input source_list_library - -\newpage - -\input source_stream_library - - \end{document} diff --git a/docs/specs/source_concurrency.tex b/docs/specs/source_concurrency.tex deleted file mode 100644 index 632810695..000000000 --- a/docs/specs/source_concurrency.tex +++ /dev/null @@ -1,9 +0,0 @@ -\subsection*{Concurrency Support} -\label{conc_supp} -The following concurrency support is provided: - -\begin{itemize} -\item \(\texttt{concurrent\_execute(}\texttt{f}_\texttt{1}, \cdots \texttt{f}_\texttt{n}\texttt{)}\): \(\textit{primitive}\), setup multiple threads for concurrent execution. For each nullary function \(\texttt{f}_\texttt{i}\) that returns \texttt{undefined}, setup a thread \(\texttt{t}_\texttt{i}\) that executes the code in the body of \(\texttt{f}_\texttt{i}\). The thread that called \texttt{concurrent\_execute} also executes concurrently with all \(\texttt{t}_\texttt{i}\). Returns \texttt{undefined}. This is an atomic operation. -\item \texttt{test\_and\_set(p)}: \(\textit{primitive}\), assumes the head of pair \texttt{p} is a boolean \(b\). Sets the head of \texttt{p} to \texttt{true}. Returns \(b\). This is an atomic operation. -\item \texttt{clear(p)}: \(\textit{primitive}\), sets the head of pair \texttt{p} to \texttt{false}. Returns \texttt{undefined}. This is an atomic operation. -\end{itemize} diff --git a/scripts/docs.mjs b/scripts/docs.mjs index 70f0faef0..bd592b606 100644 --- a/scripts/docs.mjs +++ b/scripts/docs.mjs @@ -71,20 +71,6 @@ const configs = { "pairmutator.js" ] }, - "Source §3 Concurrent": { - "readme": "README_3_CONCURRENT.md", - "dst": "source_3_concurrent/", - "libs": [ - "auxiliary.js", - "misc.js", - "math.js", - "list.js", - "stream.js", - "array.js", - "pairmutator.js", - "concurrency.js" - ] - }, "Source §3 Typed": { "readme": "README_3_TYPED.md", "dst": "source_3_typed/", @@ -190,13 +176,6 @@ const configs = { "pairmutator.js" ] }, - "CONCURRENCY": { - "readme": "README_CONCURRENCY.md", - "dst": "CONCURRENCY/", - "libs": [ - "concurrency.js" - ] - }, "MCE": { "readme": "README_MCE.md", "dst": "MCE/", diff --git a/src/constants.ts b/src/constants.ts index 6b04dd92a..00d6e3ed5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -35,7 +35,6 @@ export const sourceLanguages: Language[] = [ { chapter: Chapter.SOURCE_2, variant: Variant.TYPED }, { chapter: Chapter.SOURCE_3, variant: Variant.DEFAULT }, { chapter: Chapter.SOURCE_3, variant: Variant.TYPED }, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT }, { chapter: Chapter.SOURCE_4, variant: Variant.DEFAULT }, { chapter: Chapter.SOURCE_4, variant: Variant.TYPED }, { chapter: Chapter.SOURCE_4, variant: Variant.EXPLICIT_CONTROL } diff --git a/src/createContext.ts b/src/createContext.ts index 705b70dcb..8525471ff 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -11,7 +11,6 @@ import { import { GLOBAL, JSSLANG_PROPERTIES } from './constants' import { call_with_current_continuation } from './cse-machine/continuations' import Heap from './cse-machine/heap' -import { AsyncScheduler } from './schedulers' import * as list from './stdlib/list' import { list_to_vector } from './stdlib/list' import { listPrelude } from './stdlib/list.prelude' @@ -121,7 +120,6 @@ const createEmptyDebugger = () => ({ it: (function* (): any { return })(), - scheduler: new AsyncScheduler() } }) diff --git a/src/editors/ace/docTooltip/index.ts b/src/editors/ace/docTooltip/index.ts index 5d0667f32..a22685f62 100644 --- a/src/editors/ace/docTooltip/index.ts +++ b/src/editors/ace/docTooltip/index.ts @@ -4,7 +4,6 @@ import * as source_1_typed from './source_1_typed.json' import * as source_2 from './source_2.json' import * as source_2_typed from './source_2_typed.json' import * as source_3 from './source_3.json' -import * as source_3_concurrent from './source_3_concurrent.json' import * as source_3_typed from './source_3_typed.json' import * as source_4 from './source_4.json' import * as source_4_typed from './source_4_typed.json' @@ -45,7 +44,6 @@ export const SourceDocumentation = { '2': resolveImportInconsistency(source_2), '2_typed': resolveImportInconsistency(source_2_typed), '3': resolveImportInconsistency(source_3), - '3_concurrent': resolveImportInconsistency(source_3_concurrent), '3_typed': resolveImportInconsistency(source_3_typed), '4': resolveImportInconsistency(source_4), '4_typed': resolveImportInconsistency(source_4_typed), diff --git a/src/index.ts b/src/index.ts index 70262b05e..799bfa19b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -253,12 +253,9 @@ export async function runFilesInContext( export function resume(result: Result): Finished | ResultError | Promise { if (result.status === 'finished' || result.status === 'error') { return result - } else if (result.status === 'suspended-cse-eval') { - const value = resumeEvaluate(result.context) - return CSEResultPromise(result.context, value) - } else { - return result.scheduler.run(result.it, result.context) - } + } + const value = resumeEvaluate(result.context) + return CSEResultPromise(result.context, value) } export function interrupt(context: Context) { diff --git a/src/modules/preprocessor/__tests__/preprocessor.ts b/src/modules/preprocessor/__tests__/preprocessor.ts index 5f0baa0af..aa6d5d307 100644 --- a/src/modules/preprocessor/__tests__/preprocessor.ts +++ b/src/modules/preprocessor/__tests__/preprocessor.ts @@ -6,7 +6,7 @@ import { mockContext } from '../../../mocks/context' import { Chapter, type RecursivePartial } from '../../../types' import { memoizedGetModuleDocsAsync } from '../../loader/loaders' import preprocessFileImports from '..' -import { sanitizeAST } from '../../../utils/ast/sanitizer' +import { sanitizeAST } from '../../../utils/testing/sanitizer' import { parse } from '../../../parser/parser' import { accessExportFunctionName, diff --git a/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts b/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts index 8bcc29aa8..b86cc64c1 100644 --- a/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts +++ b/src/modules/preprocessor/__tests__/transformers/hoistAndMergeImports.ts @@ -2,7 +2,7 @@ import { mockContext } from '../../../../mocks/context' import { parse } from '../../../../parser/parser' import { Chapter } from '../../../../types' import hoistAndMergeImports from '../../transformers/hoistAndMergeImports' -import { sanitizeAST } from '../../../../utils/ast/sanitizer' +import { sanitizeAST } from '../../../../utils/testing/sanitizer' describe('hoistAndMergeImports', () => { const assertASTsAreEqual = (actualCode: string, expectedCode: string) => { diff --git a/src/modules/preprocessor/__tests__/transformers/removeExports.ts b/src/modules/preprocessor/__tests__/transformers/removeExports.ts index 8e45a5e0e..c53f9fb44 100644 --- a/src/modules/preprocessor/__tests__/transformers/removeExports.ts +++ b/src/modules/preprocessor/__tests__/transformers/removeExports.ts @@ -2,7 +2,7 @@ import { mockContext } from '../../../../mocks/context' import { parse } from '../../../../parser/parser' import { Chapter, type Context } from '../../../../types' import removeExports from '../../transformers/removeExports' -import { sanitizeAST } from '../../../../utils/ast/sanitizer' +import { sanitizeAST } from '../../../../utils/testing/sanitizer' type TestCase = [description: string, inputCode: string, expectedCode: string] diff --git a/src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts b/src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts index 5cfb4625a..201b0f26d 100644 --- a/src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts +++ b/src/modules/preprocessor/__tests__/transformers/transformProgramToFunctionDeclaration.ts @@ -3,7 +3,7 @@ import { parse } from '../../../../parser/parser' import { defaultExportLookupName } from '../../../../stdlib/localImport.prelude' import { Chapter } from '../../../../types' import { transformProgramToFunctionDeclaration } from '../../transformers/transformProgramToFunctionDeclaration' -import { sanitizeAST } from '../../../../utils/ast/sanitizer' +import { sanitizeAST } from '../../../../utils/testing/sanitizer' describe('transformImportedFile', () => { const currentFileName = '/dir/a.js' diff --git a/src/repl/__tests__/main.ts b/src/repl/__tests__/main.ts new file mode 100644 index 000000000..6477a99a0 --- /dev/null +++ b/src/repl/__tests__/main.ts @@ -0,0 +1,20 @@ +import type { Command } from 'commander' +import { getMainCommand } from '../main' + +jest.spyOn(process, 'exit').mockImplementation(code => { + throw new Error(`process.exit called with ${code}`) +}) + +jest.spyOn(process.stdout, 'write').mockImplementation(() => true) + +describe('Make sure each subcommand can be run', () => { + const mainCommand = getMainCommand() + test.each(mainCommand.commands.map(cmd => [cmd.name(), cmd] as [string, Command]))( + 'Testing %s command', + (_, cmd) => { + return expect(cmd.parseAsync(['-h'], { from: 'user' })).rejects.toMatchInlineSnapshot( + '[Error: process.exit called with 0]' + ) + } + ) +}) \ No newline at end of file diff --git a/src/repl/__tests__/svmc.ts b/src/repl/__tests__/svmc.ts new file mode 100644 index 000000000..8ec41b934 --- /dev/null +++ b/src/repl/__tests__/svmc.ts @@ -0,0 +1,110 @@ +import { asMockedFunc } from '../../utils/testing/misc' +import { compileToChoices, getSVMCCommand } from '../svmc' +import * as vm from '../../vm/svml-compiler' +import * as fs from 'fs/promises' +import { INTERNAL_FUNCTIONS } from '../../stdlib/vm.prelude' +import { expectWritten, getCommandRunner } from './utils' + +jest.mock('fs/promises', () => ({ + writeFile: jest.fn(), + readFile: jest.fn() +})) + +const mockedReadFile = asMockedFunc(fs.readFile) +const mockedWriteFile = asMockedFunc(fs.writeFile) + +jest.spyOn(vm, 'compileToIns') + +beforeEach(() => { + jest.clearAllMocks() +}) + +const { expectError: rawExpectError, expectSuccess: rawExpectSuccess } = + getCommandRunner(getSVMCCommand) + +async function expectSuccess(code: string, ...args: string[]) { + mockedReadFile.mockResolvedValueOnce(code) + + await rawExpectSuccess(...args) + expect(fs.readFile).toHaveBeenCalledTimes(1) + expect(fs.writeFile).toHaveBeenCalledTimes(1) +} + +function expectError(code: string, ...args: string[]) { + mockedReadFile.mockResolvedValueOnce(code) + return rawExpectError(...args) +} + +test('Running with defaults', async () => { + await expectSuccess('1+1;', 'test.js') + + const [[fileName]] = mockedWriteFile.mock.calls + expect(fileName).toEqual('test.svm') +}) + +it("won't run if the program has parsing errors", async () => { + await expectError('1 + 1', '/test.js') + expect(vm.compileToIns).toHaveBeenCalledTimes(0) + expectWritten(process.stderr.write).toMatchInlineSnapshot( + `"Line 1: Missing semicolon at the end of statement"` + ) +}) + +it("won't perform compilation if the output type is 'ast'", async () => { + await expectSuccess('1+1;', 'test.js', '-t', 'ast') + expect(vm.compileToIns).toHaveBeenCalledTimes(0) +}) + +describe('--internals option', () => { + test('with valid values', async () => { + await expectSuccess('1+1;', 'test.js', '--internals', '["func1", "func2"]') + expect(vm.compileToIns).toHaveBeenCalledTimes(1) + const [[, , internals]] = asMockedFunc(vm.compileToIns).mock.calls + + expect(internals).toEqual(['func1', 'func2']) + }) + + test('with non-string values in array', async () => { + await expectError('1+1;', 'test.js', '--internals', '[1, 2]') + expectWritten(process.stderr.write).toMatchInlineSnapshot(` + "error: option '-i, --internals ' argument '[1, 2]' is invalid. Expected a JSON array of strings! + " + `) + }) + + test('with a non-array', async () => { + await expectError('1+1;', 'test.js', '--internals', '{ "a": 1, "b": 2}') + expectWritten(process.stderr.write).toMatchInlineSnapshot(` + "error: option '-i, --internals ' argument '{ \\"a\\": 1, \\"b\\": 2}' is invalid. Expected a JSON array of strings! + " + `) + }) + + it('is ignored if variant is concurrent', async () => { + await expectSuccess( + '1+1;', + 'test.js', + '--internals', + '["func1", "func2"]', + '--variant', + 'concurrent' + ) + + expect(vm.compileToIns).toHaveBeenCalledTimes(1) + const [[, , internals]] = asMockedFunc(vm.compileToIns).mock.calls + const expectedNames = INTERNAL_FUNCTIONS.map(([name]) => name) + expect(internals).toEqual(expectedNames) + }) +}) + +describe('Test output options', () => { + compileToChoices.forEach(choice => { + test(choice, async () => { + await expectSuccess('1 + 1;', 'test.js', '-t', choice) + const [[fileName, contents]] = mockedWriteFile.mock.calls + + expect((fileName as string).startsWith('test')).toEqual(true) + expect(contents).toMatchSnapshot() + }) + }) +}) \ No newline at end of file diff --git a/src/repl/__tests__/transpiler.ts b/src/repl/__tests__/transpiler.ts new file mode 100644 index 000000000..34862d6d1 --- /dev/null +++ b/src/repl/__tests__/transpiler.ts @@ -0,0 +1,66 @@ +import { asMockedFunc } from '../../utils/testing/misc' +import { getTranspilerCommand } from '../transpiler' +import * as fs from 'fs/promises' +import { expectWritten, getCommandRunner } from './utils' + +jest.mock('fs/promises', () => ({ + readFile: jest.fn(), + writeFile: jest.fn() +})) + +beforeEach(() => { + jest.clearAllMocks() +}) + +const mockedWriteFile = asMockedFunc(fs.writeFile) +const mockedReadFile = asMockedFunc(fs.readFile) +const { expectError, expectSuccess } = getCommandRunner(getTranspilerCommand) + +test('Nothing should be written if the program has parser errors', async () => { + mockedReadFile.mockResolvedValueOnce('1+1') + await expectError('/test.js') + expect(fs.writeFile).toHaveBeenCalledTimes(0) + + expectWritten(process.stderr.write).toMatchInlineSnapshot( + `"[/test.js] Line 1: Missing semicolon at the end of statement"` + ) +}) + +test('Nothing should be written if the program has transpiler errors', async () => { + mockedReadFile.mockResolvedValueOnce('a;') + await expectError('/test.js') + expect(fs.writeFile).toHaveBeenCalledTimes(0) + + expectWritten(process.stderr.write).toMatchInlineSnapshot( + `"[/test.js] Line 1: Name a not declared."` + ) +}) + +test('Nothing should be written to disk if no output file was specified', async () => { + mockedReadFile.mockResolvedValueOnce('1+1;') + await expectSuccess('test.js') + expect(fs.writeFile).toHaveBeenCalledTimes(0) + + // Code should have been written to stdout + expectWritten(process.stdout.write).toMatchSnapshot() +}) + +test('Writing to file', async () => { + mockedReadFile.mockResolvedValueOnce('1+1;') + await expectSuccess('test.js', '-o', 'out.js') + expect(fs.writeFile).toHaveBeenCalledTimes(1) + + const [[fileName, contents]] = mockedWriteFile.mock.calls + expect(fileName).toEqual('out.js') + expect(contents).toMatchSnapshot() +}) + +test('pretranspile option', async () => { + mockedReadFile.mockResolvedValueOnce('1+1;') + await expectSuccess('test.js', '-o', 'out.js', '-p') + expect(fs.writeFile).toHaveBeenCalledTimes(1) + + const [[fileName, contents]] = mockedWriteFile.mock.calls + expect(fileName).toEqual('out.js') + expect(contents).toEqual('1 + 1;\n') +}) \ No newline at end of file diff --git a/src/repl/__tests__/utils.ts b/src/repl/__tests__/utils.ts new file mode 100644 index 000000000..5cc73689e --- /dev/null +++ b/src/repl/__tests__/utils.ts @@ -0,0 +1,38 @@ +import type { Command } from '@commander-js/extra-typings' +import { asMockedFunc } from '../../utils/testing/misc' + +/** + * Set up the environment for testing the given command. Returns + * `expectSuccess` and `expectError` for use with making assertions + * about the behaviour of the command + */ +export function getCommandRunner>(getter: () => T) { + jest.spyOn(process.stdout, 'write').mockImplementation(() => true) + jest.spyOn(process.stderr, 'write').mockImplementation(() => true) + jest.spyOn(process, 'exit').mockImplementation(code => { + throw new Error(`process.exit called with ${code}`) + }) + + async function runner(...args: string[]) { + await getter().parseAsync(args, { from: 'user' }) + } + + return { + expectError(...args: string[]) { + // Error conditions should always cause commands to call + // process.exit(1) + return expect(runner(...args)).rejects.toMatchInlineSnapshot( + `[Error: process.exit called with 1]` + ) + }, + expectSuccess(...args: string[]) { + return expect(runner(...args)).resolves.toBeUndefined() + } + } +} + +export function expectWritten(f: (contents: string) => any) { + expect(f).toHaveBeenCalledTimes(1) + const [[contents]] = asMockedFunc(f).mock.calls + return expect(contents) +} \ No newline at end of file diff --git a/src/repl/index.ts b/src/repl/index.ts index 72065bc95..14c3f211a 100644 --- a/src/repl/index.ts +++ b/src/repl/index.ts @@ -1,11 +1,4 @@ -#!/usr/bin/env node +#!/bin/env/node +import { getMainCommand } from './main' -import { Command } from '@commander-js/extra-typings' - -import { getReplCommand } from './repl' -import { transpilerCommand } from './transpiler' - -new Command() - .addCommand(transpilerCommand) - .addCommand(getReplCommand(), { isDefault: true }) - .parseAsync() +getMainCommand().parseAsync() \ No newline at end of file diff --git a/src/repl/main.ts b/src/repl/main.ts new file mode 100644 index 000000000..3bfcbdf80 --- /dev/null +++ b/src/repl/main.ts @@ -0,0 +1,11 @@ +import { Command } from '@commander-js/extra-typings' + +import { getSVMCCommand } from './svmc' +import { getReplCommand } from './repl' +import { getTranspilerCommand } from './transpiler' + +export const getMainCommand = () => + new Command() + .addCommand(getSVMCCommand()) + .addCommand(getTranspilerCommand()) + .addCommand(getReplCommand(), { isDefault: true }) \ No newline at end of file diff --git a/src/repl/svmc.ts b/src/repl/svmc.ts new file mode 100644 index 000000000..e285a01cf --- /dev/null +++ b/src/repl/svmc.ts @@ -0,0 +1,106 @@ +import type pathlib from 'path' +import type fslib from 'fs/promises' + +import { Command, InvalidArgumentError, Option } from '@commander-js/extra-typings' +import { createEmptyContext } from '../createContext' +import { parse } from '../parser/parser' +import { Chapter, Variant } from '../types' +import { stripIndent } from '../utils/formatters' +import { parseError } from '..' +import { assemble } from '../vm/svml-assembler' +import { compileToIns } from '../vm/svml-compiler' +import { stringifyProgram } from '../vm/util' +import { chapterParser, getChapterOption } from './utils' + +export const compileToChoices = ['ast', 'binary', 'debug', 'json'] as const + +export const getSVMCCommand = () => + new Command('svmc') + .argument('', 'File to read code from') + .addOption(getChapterOption(Chapter.SOURCE_3, chapterParser)) + .addOption( + new Option( + '-t, --compileTo ', + stripIndent` + json: Compile only, but don't assemble. + binary: Compile and assemble. + debug: Compile and pretty-print the compiler output. For debugging the compiler. + ast: Parse and pretty-print the AST. For debugging the parser.` + ) + .choices(compileToChoices) + .default('binary' as (typeof compileToChoices)[number]) + ) + .option( + '-o, --out ', + stripIndent` + Sets the output filename. + Defaults to the input filename, minus any file extension, plus '.svm'. + ` + ) + .addOption( + new Option( + '-i, --internals ', + `Sets the list of VM-internal functions. The argument should be a JSON array of +strings containing the names of the VM-internal functions.` + ) + .argParser(value => { + const parsed = JSON.parse(value) + if (!Array.isArray(parsed)) { + throw new InvalidArgumentError('Expected a JSON array of strings!') + } + + for (const each of parsed) { + if (typeof each !== 'string') { + throw new InvalidArgumentError('Expected a JSON array of strings!') + } + } + return parsed as string[] + }) + .default([] as string[]) + ) + .action(async (inputFile, opts) => { + const fs: typeof fslib = require('fs/promises') + const vmInternalFunctions = opts.internals || [] + + const source = await fs.readFile(inputFile, 'utf-8') + const context = createEmptyContext(opts.chapter, Variant.DEFAULT, [], null) + const program = parse(source, context) + if (program === null) { + process.stderr.write(parseError(context.errors)) + process.exit(1) + } + + let output: string | Uint8Array + let ext: string + + if (opts.compileTo === 'ast') { + output = JSON.stringify(program, undefined, 2) + ext = '.json' + } else { + const compiled = compileToIns(program, undefined, vmInternalFunctions) + switch (opts.compileTo) { + case 'debug': { + output = stringifyProgram(compiled).trimEnd() + ext = '.svm' + break + } + case 'json': { + output = JSON.stringify(compiled) + ext = '.json' + break + } + case 'binary': { + output = assemble(compiled) + ext = '.svm' + break + } + } + } + + const { extname, basename }: typeof pathlib = require('path') + const extToRemove = extname(inputFile) + + const outputFileName = opts.out ?? `${basename(inputFile, extToRemove)}${ext}` + await fs.writeFile(outputFileName, output) + console.log(`Output written to ${outputFileName}`) + }) \ No newline at end of file diff --git a/src/repl/transpiler.ts b/src/repl/transpiler.ts index 0f4884d1c..3ab8c9708 100644 --- a/src/repl/transpiler.ts +++ b/src/repl/transpiler.ts @@ -16,63 +16,63 @@ import { validateChapterAndVariantCombo } from './utils' -export const transpilerCommand = new Command('transpiler') - .addOption(getVariantOption(Variant.DEFAULT, [Variant.DEFAULT, Variant.NATIVE])) - .addOption(getChapterOption(Chapter.SOURCE_4, chapterParser)) - .option( - '-p, --pretranspile', - "only pretranspile (e.g. GPU -> Source) and don't perform Source -> JS transpilation" - ) - .option('-o, --out ', 'Specify a file to write to') - .argument('') - .action(async (fileName, opts) => { - if (!validateChapterAndVariantCombo(opts)) { - console.log('Invalid language combination!') - return - } - - const fs: typeof fslib = require('fs/promises') - const context = createContext(opts.chapter, opts.variant) - const entrypointFilePath = resolve(fileName) - - const linkerResult = await parseProgramsAndConstructImportGraph( - async p => { - try { - const text = await fs.readFile(p, 'utf-8') - return text - } catch (error) { - if (error.code === 'ENOENT') return undefined - throw error - } - }, - entrypointFilePath, - context, - {}, - true +export const getTranspilerCommand = () => + new Command('transpiler') + .addOption(getVariantOption(Variant.DEFAULT, [Variant.DEFAULT, Variant.NATIVE])) + .addOption(getChapterOption(Chapter.SOURCE_4, chapterParser)) + .option( + '-p, --pretranspile', + "only pretranspile and don't perform Source -> JS transpilation" ) + .option('-o, --out ', 'Specify a file to write to') + .argument('') + .action(async (fileName, opts) => { + if (!validateChapterAndVariantCombo(opts)) { + console.log('Invalid language combination!') + return + } - if (!linkerResult.ok) { - console.log(parseError(context.errors, linkerResult.verboseErrors)) - return - } + const fs: typeof fslib = require('fs/promises') + const context = createContext(opts.chapter, opts.variant) + const entrypointFilePath = resolve(fileName) - const { programs, topoOrder } = linkerResult - const bundledProgram = defaultBundler(programs, entrypointFilePath, topoOrder, context) + const linkerResult = await parseProgramsAndConstructImportGraph( + async p => { + try { + const text = await fs.readFile(p, 'utf-8') + return text + } catch (error) { + if (error.code === 'ENOENT') return undefined + throw error + } + }, + entrypointFilePath, + context, + {}, + true + ) - const transpiled = opts.pretranspile - ? generate(bundledProgram) - : transpile(bundledProgram, context).transpiled + if (!linkerResult.ok) { + process.stderr.write(parseError(context.errors, linkerResult.verboseErrors)) + process.exit(1) + } - if (context.errors.length > 0) { - console.log(parseError(context.errors, linkerResult.verboseErrors)) - return - } + const { programs, topoOrder } = linkerResult + const bundledProgram = defaultBundler(programs, entrypointFilePath, topoOrder, context) - if (opts.out) { - const resolvedOut = resolve(opts.out) - await fs.writeFile(resolvedOut, transpiled) - console.log(`Code written to ${resolvedOut}`) - } else { - console.log(transpiled) - } - }) + try { + const transpiled = opts.pretranspile + ? generate(bundledProgram) + : transpile(bundledProgram, context).transpiled + + if (opts.out) { + await fs.writeFile(opts.out, transpiled) + console.log(`Code written to ${opts.out}`) + } else { + process.stdout.write(transpiled) + } + } catch (error) { + process.stderr.write(parseError([error], linkerResult.verboseErrors)) + process.exit(1) + } + }) diff --git a/src/runner/sourceRunner.ts b/src/runner/sourceRunner.ts index e2e91befe..3795a3d1e 100644 --- a/src/runner/sourceRunner.ts +++ b/src/runner/sourceRunner.ts @@ -3,19 +3,17 @@ import * as _ from 'lodash' import type { RawSourceMap } from 'source-map' import { type IOptions, type Result } from '..' -import { JSSLANG_PROPERTIES, UNKNOWN_LOCATION } from '../constants' +import { JSSLANG_PROPERTIES } from '../constants' import { CSEResultPromise, evaluate as CSEvaluate } from '../cse-machine/interpreter' import { ExceptionError } from '../errors/errors' import { RuntimeSourceError } from '../errors/runtimeSourceError' import { TimeoutError } from '../errors/timeoutErrors' import { isPotentialInfiniteLoop } from '../infiniteLoops/errors' import { testForInfiniteLoop } from '../infiniteLoops/runtime' -import { evaluateProgram as evaluate } from '../interpreter/interpreter' import preprocessFileImports from '../modules/preprocessor' import { defaultAnalysisOptions } from '../modules/preprocessor/analyzer' import { defaultLinkerOptions } from '../modules/preprocessor/linker' import { parse } from '../parser/parser' -import { AsyncScheduler, PreemptiveScheduler } from '../schedulers' import { callee, getEvaluationSteps, @@ -25,12 +23,11 @@ import { } from '../stepper/stepper' import { sandboxedEval } from '../transpiler/evalContainer' import { transpile } from '../transpiler/transpiler' -import { Chapter, type Context, type RecursivePartial, type Scheduler, Variant } from '../types' +import { Chapter, type Context, type RecursivePartial, Variant } from '../types' import { validateAndAnnotate } from '../validator/validator' -import { compileForConcurrent } from '../vm/svml-compiler' -import { runWithProgram } from '../vm/svml-machine' import type { FileGetter } from '../modules/moduleTypes' import { mapResult } from '../alt-langs/mapper' +import assert from '../utils/assert' import { toSourceError } from './errors' import { fullJSRunner } from './fullJSRunner' import { determineExecutionMethod, determineVariant, resolvedErrorPromise } from './utils' @@ -60,29 +57,6 @@ let previousCode: { } | null = null let isPreviousCodeTimeoutError = false -function runConcurrent(program: es.Program, context: Context, options: IOptions): Promise { - if (context.shouldIncreaseEvaluationTimeout) { - context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy - } else { - context.nativeStorage.maxExecTime = options.originalMaxExecTime - } - - try { - return Promise.resolve({ - status: 'finished', - context, - value: runWithProgram(compileForConcurrent(program, context), context) - }) - } catch (error) { - if (error instanceof RuntimeSourceError || error instanceof ExceptionError) { - context.errors.push(error) // use ExceptionErrors for non Source Errors - return resolvedErrorPromise - } - context.errors.push(new ExceptionError(error, UNKNOWN_LOCATION)) - return resolvedErrorPromise - } -} - function runSubstitution( program: es.Program, context: Context, @@ -110,17 +84,6 @@ function runSubstitution( }) } -function runInterpreter(program: es.Program, context: Context, options: IOptions): Promise { - let it = evaluate(program, context) - let scheduler: Scheduler - if (options.scheduler === 'async') { - scheduler = new AsyncScheduler() - } else { - scheduler = new PreemptiveScheduler(options.steps) - } - return scheduler.run(it, context) -} - async function runNative( program: es.Program, context: Context, @@ -226,10 +189,6 @@ async function sourceRunner( return resolvedErrorPromise } - if (context.variant === Variant.CONCURRENT) { - return runConcurrent(program, context, theOptions) - } - if (theOptions.useSubst) { return runSubstitution(program, context, theOptions) } @@ -238,7 +197,7 @@ async function sourceRunner( // native, don't evaluate prelude if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) { - return await fullJSRunner(program, context, theOptions.importOptions) + return fullJSRunner(program, context, theOptions.importOptions) } // All runners after this point evaluate the prelude. @@ -264,11 +223,8 @@ async function sourceRunner( return runCSEMachine(program, context, theOptions) } - if (context.executionMethod === 'native') { - return runNative(program, context, theOptions) - } - - return runInterpreter(program, context, theOptions) + assert(context.executionMethod !== 'auto', 'Execution method should have been properly determined!') + return runNative(program, context, theOptions) } /** diff --git a/src/schedulers.ts b/src/schedulers.ts deleted file mode 100644 index 94a521f7c..000000000 --- a/src/schedulers.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* tslint:disable:max-classes-per-file */ -import { MaximumStackLimitExceeded } from './errors/errors' -import { saveState } from './stdlib/inspector' -import { Context, Result, Scheduler, Value } from './types' - -export class AsyncScheduler implements Scheduler { - public run(it: IterableIterator, context: Context): Promise { - return new Promise((resolve, _reject) => { - context.runtime.isRunning = true - let itValue = it.next() - try { - while (!itValue.done) { - itValue = it.next() - if (context.runtime.break) { - saveState(context, it, this) - itValue.done = true - } - } - } catch (e) { - resolve({ status: 'error' }) - } finally { - context.runtime.isRunning = false - } - if (context.runtime.break) { - resolve({ - status: 'suspended', - it, - scheduler: this, - context - }) - } else { - resolve({ status: 'finished', context, value: itValue.value }) - } - }) - } -} - -export class PreemptiveScheduler implements Scheduler { - constructor(public steps: number) {} - - public run(it: IterableIterator, context: Context): Promise { - return new Promise((resolve, _reject) => { - context.runtime.isRunning = true - // This is used in the evaluation of the REPL during a paused state. - // The debugger is turned off while the code evaluates just above the debugger statement. - let actuallyBreak: boolean = false - let itValue = it.next() - const interval = setInterval(() => { - let step = 0 - try { - while (!itValue.done && step < this.steps) { - step++ - itValue = it.next() - - actuallyBreak = context.runtime.break && context.runtime.debuggerOn - if (actuallyBreak) { - itValue.done = true - } - } - saveState(context, it, this) - } catch (e) { - checkForStackOverflow(e, context) - context.runtime.isRunning = false - clearInterval(interval) - resolve({ status: 'error' }) - } - if (itValue.done) { - context.runtime.isRunning = false - clearInterval(interval) - if (actuallyBreak) { - resolve({ - status: 'suspended', - it, - scheduler: this, - context - }) - } else { - resolve({ status: 'finished', context, value: itValue.value }) - } - } - }) - }) - } -} - -/* Checks if the error is a stackoverflow error, and captures it in the - context if this is the case */ -function checkForStackOverflow(error: any, context: Context) { - if (/Maximum call stack/.test(error.toString())) { - const environments = context.runtime.environments - const stacks: any = [] - let counter = 0 - for ( - let i = 0; - counter < MaximumStackLimitExceeded.MAX_CALLS_TO_SHOW && i < environments.length; - i++ - ) { - if (environments[i].callExpression) { - stacks.unshift(environments[i].callExpression) - counter++ - } - } - context.errors.push(new MaximumStackLimitExceeded(context.runtime.nodes[0], stacks)) - } -} diff --git a/src/stdlib/inspector.ts b/src/stdlib/inspector.ts index 85a6fecb3..8eeeea12f 100644 --- a/src/stdlib/inspector.ts +++ b/src/stdlib/inspector.ts @@ -1,29 +1,9 @@ -import { Context, Result } from '..' -import { Node, Scheduler, Value } from '../types' - -export const saveState = ( - context: Context, - it: IterableIterator, - scheduler: Scheduler -): void => { - context.debugger.state.it = it - context.debugger.state.scheduler = scheduler -} +import type { Context, Node } from '../types' export const setBreakpointAtLine = (lines: string[]): void => { breakpoints = lines } -export const manualToggleDebugger = (context: Context): Result => { - context.runtime.break = true - return { - status: 'suspended', - scheduler: context.debugger.state.scheduler, - it: context.debugger.state.it, - context - } -} - let breakpoints: string[] = [] let moved: boolean = true let prevStoppedLine: number = -1 diff --git a/src/transpiler/__tests__/transpiled-code.ts b/src/transpiler/__tests__/transpiled-code.ts index 5b54c21cb..52688a80b 100644 --- a/src/transpiler/__tests__/transpiled-code.ts +++ b/src/transpiler/__tests__/transpiled-code.ts @@ -2,7 +2,7 @@ import { mockContext } from '../../mocks/context' import { parse } from '../../parser/parser' import { Chapter } from '../../types' import * as ast from '../../utils/ast/astCreator' -import { sanitizeAST } from '../../utils/ast/sanitizer' +import { sanitizeAST } from '../../utils/testing/sanitizer' import { stripIndent } from '../../utils/formatters' import { transformImportDeclarations, transpile } from '../transpiler' diff --git a/src/types.ts b/src/types.ts index 81456b691..b68dcd46b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,7 +64,7 @@ export interface Comment { loc: SourceLocation | undefined } -export type ExecutionMethod = 'native' | 'interpreter' | 'auto' | 'cse-machine' +export type ExecutionMethod = 'native' | 'auto' | 'cse-machine' export enum Chapter { SOURCE_1 = 1, @@ -94,7 +94,6 @@ export enum Variant { TYPED = 'typed', NATIVE = 'native', WASM = 'wasm', - CONCURRENT = 'concurrent', EXPLICIT_CONTROL = 'explicit-control' } @@ -168,7 +167,6 @@ export interface Context { status: boolean state: { it: IterableIterator - scheduler: Scheduler } } @@ -274,23 +272,12 @@ export interface Finished { // field instead } -export interface Suspended { - status: 'suspended' - it: IterableIterator - scheduler: Scheduler - context: Context -} - export interface SuspendedCseEval { status: 'suspended-cse-eval' context: Context } -export type Result = Suspended | Finished | Error | SuspendedCseEval - -export interface Scheduler { - run(it: IterableIterator, context: Context): Promise -} +export type Result = Finished | Error | SuspendedCseEval /** * StatementSequence : A sequence of statements not surrounded by braces. @@ -510,3 +497,5 @@ export type RecursivePartial = T extends Array [K in keyof T]: RecursivePartial }> : T + +export type NodeTypeToNode = Extract \ No newline at end of file diff --git a/src/utils/testing.ts b/src/utils/testing/index.ts similarity index 96% rename from src/utils/testing.ts rename to src/utils/testing/index.ts index f26de4bdc..d8ef83972 100644 --- a/src/utils/testing.ts +++ b/src/utils/testing/index.ts @@ -1,11 +1,11 @@ import type { MockedFunction } from 'jest-mock' -import createContext, { defineBuiltin } from '../createContext' -import { parseError, Result, runInContext } from '../index' -import { mockContext } from '../mocks/context' -import { ImportOptions } from '../modules/moduleTypes' -import { parse } from '../parser/parser' -import { transpile } from '../transpiler/transpiler' +import createContext, { defineBuiltin } from '../../createContext' +import { parseError, Result, runInContext } from '../../' +import { mockContext } from '../../mocks/context' +import type { ImportOptions } from '../../modules/moduleTypes' +import { parse } from '../../parser/parser' +import { transpile } from '../../transpiler/transpiler' import { Chapter, Context, @@ -14,8 +14,8 @@ import { Value, Variant, type Finished -} from '../types' -import { stringify } from './stringify' +} from '../../types' +import { stringify } from '../stringify' export interface CodeSnippetTestCase { name: string diff --git a/src/utils/testing/misc.ts b/src/utils/testing/misc.ts new file mode 100644 index 000000000..4ca9fc519 --- /dev/null +++ b/src/utils/testing/misc.ts @@ -0,0 +1,27 @@ +import type { MockedFunction } from 'jest-mock' +import type { Result } from '../..' +import type { Finished, Value, Node, NodeTypeToNode } from '../../types' + +export function asMockedFunc any>(func: T) { + return func as MockedFunction +} + +export function expectTrue(cond: boolean): asserts cond { + expect(cond).toEqual(true) +} + +export function expectFinishedResult(result: Result): asserts result is Finished { + expect(result.status).toEqual('finished') +} + +export function expectFinishedResultValue(result: Result, value: Value) { + expectFinishedResult(result) + expect(result.value).toEqual(value) +} + +export function expectNodeType( + typeStr: T, + node: Node +): asserts node is NodeTypeToNode { + expect(node.type).toEqual(typeStr) +} \ No newline at end of file diff --git a/src/utils/ast/sanitizer.ts b/src/utils/testing/sanitizer.ts similarity index 100% rename from src/utils/ast/sanitizer.ts rename to src/utils/testing/sanitizer.ts diff --git a/src/vm/__tests__/svml-machine.ts b/src/vm/__tests__/svml-machine.ts deleted file mode 100644 index 8d94829b9..000000000 --- a/src/vm/__tests__/svml-machine.ts +++ /dev/null @@ -1,1577 +0,0 @@ -import { Chapter, Variant } from '../../types' -import { stripIndent } from '../../utils/formatters' -import { - expectDisplayResult, - expectParsedError, - expectResult, - expectVisualiseListResult, - getDisplayResult, - snapshotSuccess -} from '../../utils/testing' - -// concurrent programs return undefined so use display -// for tests instead -// all tests assumes display works -// comments mention additional opcodes tested by test code -describe('standard opcodes', () => { - test('LGCI works', () => { - return expectDisplayResult(`display(123);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "123", - ] - `) - }) - - test('LGCF64 works', () => { - return expectDisplayResult(`display(1.5);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "1.5", - ] - `) - }) - - test('LGCB0 works', () => { - return expectDisplayResult(`display(false);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "false", - ] - `) - }) - - test('LGCB1 works', () => { - return expectDisplayResult(`display(true);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "true", - ] - `) - }) - - test('LGCU works', () => { - return expectDisplayResult(`display(undefined);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "undefined", - ] - `) - }) - - test('LGCN works', () => { - return expectDisplayResult(`display(null);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "null", - ] - `) - }) - - test('LGCS works', () => { - return expectDisplayResult(`display("test string");`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "\\"test string\\"", - ] - `) - }) - - test('ADDG works for numbers', () => { - return expectDisplayResult('display(-1+1);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "0", - ] - `) - }) - - test('ADDG works for strings', () => { - return expectDisplayResult('display("first"+"second");', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "\\"firstsecond\\"", - ] - `) - }) - - test('ADDG fails for ill-typed operands', () => { - return expectParsedError('1+undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected string and string or number and number, got number and undefined for +."` - ) - }) - - test('SUBG works for numbers', () => { - return expectDisplayResult('display(123-124);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "-1", - ] - `) - }) - - test('SUBG fails for ill-typed operands', () => { - return expectParsedError('1-undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected number and number, got number and undefined for -."` - ) - }) - - test('MULG works for numbers', () => { - return expectDisplayResult('display(123*2);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "246", - ] - `) - }) - - test('MULG fails for ill-typed operands', () => { - return expectParsedError('1*undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected number and number, got number and undefined for *."` - ) - }) - - test('DIVG works for numbers', () => { - return expectDisplayResult('display(128/32);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "4", - ] - `) - }) - - test('DIVG fails for division by 0', () => { - return expectParsedError('128/0;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: execution aborted: division by 0"`) - }) - - test('DIVG fails for ill-typed operands', () => { - return expectParsedError('1/undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected number and number, got number and undefined for /."` - ) - }) - - test('MODG works for numbers', () => { - return expectDisplayResult('display(128%31);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "4", - ] - `) - }) - - test('MODG fails for ill-typed operands', () => { - return expectParsedError('1%undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected undefined, got undefined for undefined."` - ) - }) - - test('NEGG works', () => { - return expectDisplayResult('display(-1);display(-(-1));', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "-1", - "1", - ] - `) - }) - - test('NEGG fails for ill-typed operands', () => { - return expectParsedError('-"hi";', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: execution aborted: Expected number, got string for -."`) - }) - - test('NOTG works', () => { - return expectDisplayResult('display(!false);display(!true);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('NOTG fails for ill-typed operands', () => { - return expectParsedError('!1;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: execution aborted: Expected boolean, got number for !."`) - }) - - test('LTG works for numbers', () => { - return expectDisplayResult('display(5 < 10); display(10 < 5);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('LTG works for strings', () => { - return expectDisplayResult('display("abc" < "bcd"); display("bcd" < "abc");', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('LTG fails for ill-typed operands', () => { - return expectParsedError('1 { - return expectDisplayResult('display(5 > 10); display(10 > 5);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "false", - "true", - ] - `) - }) - - test('GTG works for strings', () => { - return expectDisplayResult('display("abc" > "bcd"); display("bcd" > "abc");', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "false", - "true", - ] - `) - }) - - test('GTG fails for ill-typed operands', () => { - return expectParsedError('1>undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected string and string or number and number, got number and undefined for >."` - ) - }) - - test('LEG works for numbers', () => { - return expectDisplayResult( - stripIndent` - display(5 <= 10); - display(5 <= 5); - display(10 <= 5); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "true", - "true", - "false", - ] - `) - }) - - test('LEG works for strings', () => { - return expectDisplayResult( - stripIndent` - display('abc' <= 'bcd'); - display('abc' <= 'abc'); - display('bcd' <= 'abc'); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "true", - "true", - "false", - ] - `) - }) - - test('LEG fails for ill-typed operands', () => { - return expectParsedError('1<=undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected string and string or number and number, got number and undefined for <=."` - ) - }) - - test('GEG works for numbers', () => { - return expectDisplayResult( - stripIndent` - display(5 >= 10); - display(5 >= 5); - display(10 >= 5); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "false", - "true", - "true", - ] - `) - }) - - test('GEG works for numbers', () => { - return expectDisplayResult( - stripIndent` - display('abc' >= 'bcd'); - display('abc' >= 'abc'); - display('bcd' >= 'abc'); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "false", - "true", - "true", - ] - `) - }) - - test('GEG fails for ill-typed operands', () => { - return expectParsedError('1>=undefined;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected string and string or number and number, got number and undefined for >=."` - ) - }) - - // NEWC, CALL, RETG - test('function and function calls work', () => { - return expectDisplayResult( - stripIndent` - function f(x) { - display(x); - return 1; - } - display(f(3)); - display(f); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "3", - "1", - "\\"\\"", - ] - `) - }) - - test('STLG and LDLG works', () => { - return expectDisplayResult(`const x = 1; display(x);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "1", - ] - `) - }) - - // NEWA, LDAG, STAG, DUP - test('array opcodes work', () => { - return expectDisplayResult(`const x = [1,2,3,1]; display(x[1]); display(x[8]);`, { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "2", - "undefined", - ] - `) - }) - - test('LDAG fails for non-array', () => { - return expectParsedError('1[0];', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected array, got number for array access."` - ) - }) - - test('LDAG fails for ill-typed argument', () => { - return expectParsedError('const arr = []; arr["hi"];', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected number, got string for array index."` - ) - }) - - test('STAG fails for non-array', () => { - return expectParsedError('0[1] = 1;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected array, got number for array access."` - ) - }) - - test('STAG fails for ill-typed argument', () => { - return expectParsedError('const arr = []; arr["hi"] = 1;', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected number, got string for array index."` - ) - }) - - test('EQG works', () => { - return expectDisplayResult( - stripIndent` - const x = [1,2]; - const f = () => {}; - const y = test_and_set; - const z = list; - display(undefined === undefined && - null === null && - null !== undefined && - true === true && - false === false && - false !== true && - 1 === 1 && - -1 === -1 && - x !== [1,2] && - x === x && - f === f && - f !== (() => {}) && - 'stringa' === 'stringa' && - 'stringa' !== 'stringb' && - true !== null && - y !== z && - z === list && - y === test_and_set && - 0 !== "0"); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - ] - `) - }) - - test('LDPG and STPG work', () => { - return expectDisplayResult( - stripIndent` - let x = 1; - display(x); - function f() { - x = 3; - } - f(); - display(x); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "1", - "3", - ] - `) - }) - - test('BRF works', () => { - return expectDisplayResult( - stripIndent` - if (true) { - display('did not BRF'); - } else {} - if (false) {} else { - display('BRF'); - } - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "\\"did not BRF\\"", - "\\"BRF\\"", - ] - `) - }) - - test('BRF works, no else', () => { - return expectDisplayResult( - stripIndent` - if (true) { - display('did not BRF'); - } - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "\\"did not BRF\\"", - ] - `) - }) - - test('BRF works, no else 2', () => { - return expectDisplayResult( - stripIndent` - if (false) { - display("should not show"); - } - display("should show"); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "\\"should show\\"", - ] - `) - }) - - // BR, NEWENV, POPENV - test('while loops works', () => { - return expectDisplayResult( - stripIndent` - let x = 0; - const y = 'before NEWENV'; - display(y); - while (x < 1) { - const y = 'after NEWENV'; - display(y); - x = x + 1; - display('before BR'); - } - display('after POPENV'); - display('after BR'); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "\\"before NEWENV\\"", - "\\"after NEWENV\\"", - "\\"before BR\\"", - "\\"after POPENV\\"", - "\\"after BR\\"", - ] - `) - }) -}) - -describe('primitive opcodes', () => { - describe('self-implemented', () => { - test('DISPLAY works for circular references', () => { - return expectDisplayResult( - stripIndent` - const p = pair(1,2); - const q = pair(3,4); - set_head(q,p); - set_tail(p,q); - display(p); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "[1, [..., 4]]", - ] - `) - }) - - test('DISPLAY throws error if no arguments', () => { - return expectParsedError( - stripIndent` - display(); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(`"Error: \\"Expected 1 or more arguments, but got 0.\\""`) - }) - - test('ARRAY_LEN works', () => { - return expectDisplayResult( - stripIndent` - const arr = []; - const arr1 = [1,2,3]; - const p = pair(1,2); - display(array_length(arr)); - display(array_length(arr1)); - arr[100] = 100; - display(array_length(arr)); - display(array_length(p)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "0", - "3", - "101", - "2", - ] - `) - }) - - test('ARRAY_LEN fails for ill-typed argument', () => { - return expectParsedError('array_length(1);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected array, got number for array_length."` - ) - }) - - test('DRAW_DATA works', () => { - return expectVisualiseListResult( - stripIndent` - draw_data(pair(true, [1])); - draw_data(null, list(undefined, 2), "3"); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - Array [ - Array [ - true, - Array [ - 1, - ], - ], - ], - Array [ - null, - Array [ - undefined, - Array [ - 2, - null, - ], - ], - "3", - ], - ] - `) - }) - - test('DRAW_DATA returns correct values', () => { - return expectDisplayResult( - stripIndent` - display(draw_data(pair(true, [1]))); - display(draw_data(null, list(undefined, 2), "3")); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "[true, [1]]", - "null", - ] - `) - }) - - test('DRAW_DATA throws error if no arguments', () => { - return expectParsedError( - stripIndent` - draw_data(); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(`"Error: \\"Expected 1 or more arguments, but got 0.\\""`) - }) - - test('ERROR works', () => { - return expectParsedError('error(123);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: 123"`) - }) - - test('IS_ARRAY works', () => { - return expectDisplayResult( - stripIndent` - display(is_array([1,2])); - display(is_array(1)); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('IS_BOOL works', () => { - return expectDisplayResult( - stripIndent` - display(is_boolean(true)); - display(is_boolean(1)); - `, - { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('IS_FUNC works', () => { - return expectDisplayResult( - stripIndent` - display(is_function(() => {})); - display(is_function(1)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('IS_NULL works', () => { - return expectDisplayResult( - stripIndent` - display(is_null(null)); - display(is_null(1)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('IS_NUMBER works', () => { - return expectDisplayResult( - stripIndent` - display(is_number(1)); - display(is_number(false)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('IS_STRING works', () => { - return expectDisplayResult( - stripIndent` - display(is_string("string")); - display(is_string(1)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('IS_UNDEFINED works', () => { - return expectDisplayResult( - stripIndent` - display(is_undefined(undefined)); - display(is_undefined(1)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - // variadic test as well - test('MATH_HYPOT works', () => { - return expectDisplayResult( - stripIndent` - display(math_hypot(3,4)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "5", - ] - `) - }) - - test('DISPLAY_LIST works', () => { - return expectDisplayResult( - stripIndent` - display_list(pair(1, null)); - display_list(pair(1, pair(2, null)), "test"); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "list(1)", - "test list(1, 2)", - ] - `) - }) - - test('CHAR_AT works', () => { - return expectDisplayResult( - stripIndent` - display(char_at("test", 1)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "\\"e\\"", - ] - `) - }) - - test('ARITY works', () => { - return expectDisplayResult( - stripIndent` - display(arity(math_random)); - display(arity(accumulate)); - display(arity(display)); - display(arity((x, y) => x)); - function f() {} - display(arity(f)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "0", - "3", - "0", - "2", - "0", - ] - `) - }) - - test('ARITY fails for ill-typed argument', () => { - return expectParsedError('arity(1);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot( - `"Error: execution aborted: Expected closure, got number for arity."` - ) - }) - - // variadic test - test('list works', () => { - return expectDisplayResult( - stripIndent` - display(list(1,2,3,4)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "[1, [2, [3, [4, null]]]]", - ] - `) - }) - - test('stream_tail fails for ill-typed arguments', () => { - return expectParsedError( - stripIndent` - stream_tail(1); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot( - `"Error: \\"stream_tail(xs) expects a pair as argument xs, but encountered 1\\""` - ) - }) - }) - - test('nullary handler', () => { - return snapshotSuccess('get_time();', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }) - }) - - test('unary handler', () => { - return expectDisplayResult( - stripIndent` - display(math_abs(-1)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "1", - ] - `) - }) - - test('binary handler', () => { - return expectDisplayResult( - stripIndent` - display(math_pow(2,3)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "8", - ] - `) - }) - - test('math constants', () => { - return expectDisplayResult( - stripIndent` - display(Infinity); - display(NaN); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "Infinity", - "NaN", - ] - `) - }) - - describe(Variant.CONCURRENT, () => { - test('TEST_AND_SET works', () => { - return expectDisplayResult( - stripIndent` - const x = list(false); - display(head(x)); - test_and_set(x); - display(head(x)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "false", - "true", - ] - `) - }) - - test('TEST_AND_SET fails for ill-typed arguments', () => { - return expectParsedError( - stripIndent` - test_and_set(1); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot( - `"Error: execution aborted: Expected array, got number for test_and_set."` - ) - }) - - test('CLEAR works', () => { - return expectDisplayResult( - stripIndent` - const x = list(true); - display(head(x)); - clear(x); - display(head(x)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - "false", - ] - `) - }) - - test('CLEAR fails for ill-typed arguments', () => { - return expectParsedError( - stripIndent` - clear(1); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(`"Error: execution aborted: Expected array, got number for clear."`) - }) - }) -}) - -describe('standard program execution', () => { - test('program always returns all threads terminated', () => { - return expectResult('1 + 1;', { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT }).toBe( - 'all threads terminated' - ) - }) - - test('arrow function definitions work', () => { - return expectDisplayResult( - stripIndent` - const f = x => { - display(x); - return 1; - }; - const g = x => display(x); - f(3); - g(true); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "3", - "true", - ] - `) - }) - - test('logical operators work', () => { - return expectDisplayResult('display(!(true && (false || (true && !false))));', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - Array [ - "false", - ] - `) - }) - - test('&& operator shortcircuit works', () => { - return snapshotSuccess( - stripIndent` - function f() { - f(); - } - false && f(); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ) - }) - - test('|| operator shortcircuit works', () => { - return snapshotSuccess( - stripIndent` - function f() { - f(); - } - true || f(); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ) - }) - - test('list functions work', () => { - return expectDisplayResult( - stripIndent` - function permutations(xs) { - return is_null(xs) - ? list(null) - : accumulate(append, - null, - map(x => map(p => pair(x, p), - permutations(remove(x,xs))), - xs)); - } - - display(permutations(list(1,2,3))); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "[ [1, [2, [3, null]]], - [ [1, [3, [2, null]]], - [ [2, [1, [3, null]]], - [[2, [3, [1, null]]], [[3, [1, [2, null]]], [[3, [2, [1, null]]], null]]]]]]", - ] - `) - }) - - // taken from Studio 11 - test('stream functions work', () => { - return expectDisplayResult( - stripIndent` - function interleave_stream_append(s1,s2) { - return is_null(s1) - ? s2 - : pair(head(s1), () => interleave_stream_append(s2, - stream_tail(s1))); - } - - function stream_pairs(s) { - return (is_null(s) || is_null(stream_tail(s))) - ? null - : pair(pair(head(s), head(stream_tail(s))), - () => interleave_stream_append( - stream_map(x => pair(head(s), x), - stream_tail(stream_tail(s))), - stream_pairs(stream_tail(s)))); - } - - const ints = integers_from(1); - const s = stream_pairs(ints); - display(eval_stream(s, 10)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "[ [1, 2], - [ [1, 3], - [ [2, 3], - [[1, 4], [[2, 4], [[1, 5], [[3, 4], [[1, 6], [[2, 5], [[1, 7], null]]]]]]]]]]", - ] - `) - }) - - test('program times out', () => { - return expectParsedError('while(true) {}', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(` - "Potential infinite loop detected. - If you are certain your program is correct, press run again without editing your program. - The time limit will be increased from 1 to 10 seconds. - This page may be unresponsive for up to 10 seconds if you do so." - `) - }) - - test('block scoping works', () => { - return expectDisplayResult( - stripIndent` - const x = 1; - function f(y) { - display(-x); - } - { - const x = 2; - function f(y) { - display(-x); - } - { - const x = 3; - if (true) { - display(x); - } else { - error('should not reach here'); - } - display(x); - f(1); - } - display(x); - } - display(x); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "3", - "3", - "-2", - "2", - "1", - ] - `) - }) - - test('block scoping works, part 2', () => { - return expectParsedError( - stripIndent` - { - let i = 5; - } - display(i); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(`"Line 4: Name i not declared."`) - }) - - test('return in loop throws error', () => { - return expectParsedError( - stripIndent` - function f() { - while(true) { - return 1; - } - } - f(); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(`"Error: return not allowed in loops"`) - }) - - test('continue and break works', () => { - return expectDisplayResult( - stripIndent` - while(true) { - break; - display(1); - } - let i = 0; - for (i; i < 2; i = i + 1) { - if (i === 1) { - continue; - } else { - display(i); - } - } - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "0", - ] - `) - }) - - test('const assignment throws error', () => { - return expectParsedError( - stripIndent` - const x = 1; - x = 2; - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(`"Line 2: Cannot assign new value to constant x."`) - }) - - test('treat primitive functions as first-class', () => { - return expectDisplayResult( - stripIndent` - const x = list; - display(x(1,2)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "[1, [2, null]]", - ] - `) - }) - - test('treat internal functions as first-class', () => { - return expectDisplayResult( - stripIndent` - const x = test_and_set; - const xs = list(false); - display(x(xs)); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "false", - ] - `) - }) - - test('wrong number of arguments for internal functions throws error', () => { - return expectParsedError( - stripIndent` - const x = list(false); - test_and_set(x, 1); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(`"Error: execution aborted: Expected 1 arguments, but got 2."`) - }) - - test('wrong number of arguments for normal functions throws error', () => { - return expectParsedError('((x, y) => 1)(1);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: execution aborted: Expected 2 arguments, but got 1."`) - }) - - test('wrong number of arguments for primitive functions throws error', () => { - return expectParsedError('math_sin(1,2);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: execution aborted: Expected 1 arguments, but got 2."`) - }) - - test('call non function value throws error', () => { - return expectParsedError('let x = 0; x(1,2);', { - chapter: Chapter.SOURCE_3, - variant: Variant.CONCURRENT - }).toMatchInlineSnapshot(`"Error: execution aborted: calling non-function value 0."`) - }) - - test('tail call for internal functions work', () => { - return expectDisplayResult( - stripIndent` - function f() { - return test_and_set(list(true)); - } - display(f()); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "true", - ] - `) - }) - - test('closures declared in for loops work', () => { - return expectDisplayResult( - stripIndent` - let f = null; - f = () => { display(-1); }; - for(let i = 0; i < 5; i = i + 1) { - if (i === 3) { - f = () => { display(i); }; - } else {} - } - f(); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "3", - ] - `) - }) - - test('nested for loops work', () => { - return expectDisplayResult( - stripIndent` - for (let i = 0; i < 10; i = i + 1) { - for (let j = 0; j < 10; j = j + 1) {} - display(i); - } - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - ] - `) - }) - - test('nested for loops with same identifier work', () => { - return expectDisplayResult( - stripIndent` - for (let i = 0; i < 3; i = i + 1) { - for (let i = 0; i < 3; i = i + 1) { - display(i, "inner"); - } - display(i, "outer"); - } - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "inner 0", - "inner 1", - "inner 2", - "outer 0", - "inner 0", - "inner 1", - "inner 2", - "outer 1", - "inner 0", - "inner 1", - "inner 2", - "outer 2", - ] - `) - }) - - test('continue in while loops works', () => { - return expectDisplayResult( - stripIndent` - let x = false; - while (true) { - if (x) { - break; - } else { - x = true; - } - continue; - } - display(0); - `, - { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT } - ).toMatchInlineSnapshot(` - Array [ - "0", - ] - `) - }) -}) - -// fails with a large enough TO -test('concurrent program execution interleaves', () => { - const code = stripIndent` - const t1 = () => { - for(let i = 0; i < 50; i = i + 1) { - display('t1'); - } - }; - const t2 = () => { - for(let i = 0; i < 50; i = i + 1) { - display('t2'); - } - }; - concurrent_execute(t1, t2); - for(let i = 0; i < 50; i = i + 1) { - display('main'); - } - ` - return getDisplayResult(code, { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT }).then( - displayResult => { - // check for interleaving displays of main, t1 and t2 - // done by looking for 't1' and 't2' somewhere between two 'main' displays - let firstMain = -1 - let foundT1 = false - let foundT2 = false - for (let i = 0; i < displayResult.length; i++) { - const currentResult = displayResult[i] - switch (currentResult) { - case '"main"': { - if (firstMain === -1) { - firstMain = i - continue - } - if (foundT1 && foundT2) { - return - } - continue - } - case '"t1"': { - if (firstMain === -1) { - continue - } - foundT1 = true - continue - } - case '"t2"': { - if (firstMain === -1) { - continue - } - foundT2 = true - continue - } - default: { - fail('Did not expect "' + currentResult + '" in output') - } - } - } - fail('Did not interleave') - } - ) -}) - -// Still fails when TO is so large that this program takes more than a second to run -test('concurrent program execution interleaves (busy wait)', () => { - const code = stripIndent` - let state = 0; - const t1 = () => { - while (state < 10) { - if (state % 3 === 0) { - state = state + 1; - } else {} - display('t1'); - } - }; - const t2 = () => { - while (state < 10) { - if (state % 3 === 1) { - state = state + 1; - } else {} - display('t2'); - } - }; - concurrent_execute(t1, t2); - while (state < 10) { - if (state % 3 === 2) { - state = state + 1; - } else {} - display('main'); - } - ` - return getDisplayResult(code, { chapter: Chapter.SOURCE_3, variant: Variant.CONCURRENT }) -}) diff --git a/src/vm/svmc.ts b/src/vm/svmc.ts deleted file mode 100644 index 0fe160ce1..000000000 --- a/src/vm/svmc.ts +++ /dev/null @@ -1,231 +0,0 @@ -import * as fs from 'fs' -import * as util from 'util' - -import { createEmptyContext } from '../createContext' -import { parse } from '../parser/parser' -import { INTERNAL_FUNCTIONS as concurrentInternalFunctions } from '../stdlib/vm.prelude' -import { Chapter, Variant } from '../types' -import { assemble } from './svml-assembler' -import { compileToIns } from './svml-compiler' -import { stringifyProgram } from './util' - -interface CliOptions { - compileTo: 'debug' | 'json' | 'binary' | 'ast' - sourceChapter: Chapter.SOURCE_1 | Chapter.SOURCE_2 | Chapter.SOURCE_3 - sourceVariant: Variant.DEFAULT | Variant.CONCURRENT // does not support other variants - inputFilename: string - outputFilename: string | null - vmInternalFunctions: string[] | null -} - -const readFileAsync = util.promisify(fs.readFile) -const writeFileAsync = util.promisify(fs.writeFile) - -// This is a console program. We're going to print. -/* tslint:disable:no-console */ - -function parseOptions(): CliOptions | null { - const ret: CliOptions = { - compileTo: 'binary', - sourceChapter: Chapter.SOURCE_3, - sourceVariant: Variant.DEFAULT, - inputFilename: '', - outputFilename: null, - vmInternalFunctions: null - } - - let endOfOptions = false - let error = false - const args = process.argv.slice(2) - while (args.length > 0) { - let option = args[0] - let argument = args[1] - let argShiftNumber = 2 - if (!endOfOptions && option.startsWith('--') && option.includes('=')) { - ;[option, argument] = option.split('=') - argShiftNumber = 1 - } - if (!endOfOptions && option.startsWith('-')) { - switch (option) { - case '--compile-to': - case '-t': - switch (argument) { - case 'debug': - case 'json': - case 'binary': - case 'ast': - ret.compileTo = argument - break - default: - console.error('Invalid argument to --compile-to: %s', argument) - error = true - break - } - args.splice(0, argShiftNumber) - break - case '--chapter': - case '-c': - const argInt = parseInt(argument, 10) - if (argInt === 1 || argInt === 2 || argInt === 3) { - ret.sourceChapter = argInt - } else { - console.error('Invalid Source chapter: %d', argInt) - error = true - } - args.splice(0, argShiftNumber) - break - case '--variant': - case '-v': - switch (argument) { - case Variant.DEFAULT: - case Variant.CONCURRENT: - ret.sourceVariant = argument - break - default: - console.error('Invalid/Unsupported Source Variant: %s', argument) - error = true - break - } - args.splice(0, argShiftNumber) - break - case '--out': - case '-o': - ret.outputFilename = argument - args.splice(0, argShiftNumber) - break - case '--internals': - case '-i': - ret.vmInternalFunctions = JSON.parse(argument) - args.splice(0, argShiftNumber) - break - case '--': - endOfOptions = true - args.shift() - break - default: - console.error('Unknown option %s', option) - args.shift() - error = true - break - } - } else { - if (ret.inputFilename === '') { - ret.inputFilename = args[0] - } else { - console.error('Excess non-option argument: %s', args[0]) - error = true - } - args.shift() - } - } - - if (ret.inputFilename === '') { - console.error('No input file specified') - error = true - } - - return error ? null : ret -} - -async function main() { - const options = parseOptions() - if (options == null) { - console.error(`Usage: svmc [options...] - -Options: --t, --compile-to