Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Use Asynchronous Code for Loading Modules #1471

Merged
merged 28 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f9cdfb7
Add async loading code
leeyi45 Aug 20, 2023
5efb8a5
Made infinite loop checking async
leeyi45 Aug 20, 2023
8c5a7ac
Update stepper to be async
leeyi45 Aug 20, 2023
82f6e60
Add assert and misc utils
leeyi45 Aug 20, 2023
78117c5
Update transpiler to be async
leeyi45 Aug 20, 2023
7999355
Update runners
leeyi45 Aug 20, 2023
065373f
Add new import options
leeyi45 Aug 20, 2023
efd0eae
Ignore tests during typechecking
leeyi45 Aug 20, 2023
606fc48
Relocate typeguards
leeyi45 Aug 20, 2023
bdac127
Update with assertions and typeguards
leeyi45 Aug 20, 2023
c27adf2
Update repl transpiler to be async
leeyi45 Aug 20, 2023
dcaf6d9
Abstract out module context initialization and tab loading
leeyi45 Aug 21, 2023
0463da1
Renamed promise timeout error and added tests
leeyi45 Aug 21, 2023
03b1909
Remove old code
leeyi45 Aug 21, 2023
6f354b6
Update options
leeyi45 Aug 21, 2023
4aab882
Add files to avoid line ending issues
leeyi45 Aug 21, 2023
a79eb15
Use POSIX paths on Windows systems
ianyong Aug 21, 2023
39857cd
Ran format
leeyi45 Aug 22, 2023
7222d5d
Update eslint rules to follow exclusion of tests
leeyi45 Aug 22, 2023
85bc39e
Incorporate changes for posix path handling
leeyi45 Aug 22, 2023
6d88e29
Merge remote-tracking branch 'origin/use-posix-paths' into module-loa…
leeyi45 Aug 22, 2023
4d12e67
Merge remote-tracking branch 'origin/master' into module-loader-async
leeyi45 Sep 2, 2023
b905d4a
Merge commit '532f16f01114dbb53bcc1e5fb1acc4a1680a7d59' into module-l…
leeyi45 Sep 4, 2023
ab82c54
Minor change to resolve certain webpack issues with the frontend
leeyi45 Sep 4, 2023
950c1ad
Use a different posix path import
leeyi45 Sep 5, 2023
ca642bc
Allow loading of export default tabs
leeyi45 Sep 5, 2023
1be50cc
Refactor collation of import declarations
leeyi45 Sep 5, 2023
9c08022
Merge branch 'module-loader-async' of github.com:source-academy/js-sl…
leeyi45 Sep 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*]
end_of_line = lf
10 changes: 9 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"ignorePatterns": ["src/**/__tests__/**", "src/**/__mocks__/**"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
Expand Down Expand Up @@ -45,5 +46,12 @@
"@typescript-eslint/unbound-method": "off",
"prefer-rest-params": "off",
"simple-import-sort/imports": "warn"
}
},
"overrides": [{
"files": ["src/**/__tests__/**", "src/**/__mocks__/**"],
"plugins": ["simple-import-sort"],
"rules": {
"simple-import-sort/imports": "warn"
}
}]
}
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/src/scm-slang/
/src/py-slang/
/src/py-slang/
/src/**/__tests__/**/__snapshots__
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"printWidth": 100,
"parser": "typescript",
"trailingComma": "none",
"arrowParens": "avoid"
"arrowParens": "avoid",
"endOfLine": "lf"
}
70 changes: 31 additions & 39 deletions src/ec-evaluator/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@

/* tslint:disable:max-classes-per-file */
import * as es from 'estree'
import { partition, uniqueId } from 'lodash'
import { uniqueId } from 'lodash'

import { IOptions } from '..'
import { UNKNOWN_LOCATION } from '../constants'
import * as errors from '../errors/errors'
import { RuntimeSourceError } from '../errors/runtimeSourceError'
import Closure from '../interpreter/closure'
import { UndefinedImportError } from '../modules/errors'
import { loadModuleBundle, loadModuleTabs } from '../modules/moduleLoader'
import { ModuleFunctions } from '../modules/moduleTypes'
import { initModuleContext, loadModuleBundle } from '../modules/moduleLoader'
import { ImportTransformOptions } from '../modules/moduleTypes'
import { checkEditorBreakpoints } from '../stdlib/inspector'
import { Context, ContiguousArrayElements, Result, Value } from '../types'
import assert from '../utils/assert'
import { filterImportDeclarations } from '../utils/ast/helpers'
import * as ast from '../utils/astCreator'
import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators'
import * as rttc from '../utils/rttc'
Expand Down Expand Up @@ -172,45 +174,31 @@
function evaluateImports(
program: es.Program,
context: Context,
loadTabs: boolean,
checkImports: boolean
{ loadTabs, checkImports }: ImportTransformOptions
) {
const [importNodes] = partition(program.body, ({ type }) => type === 'ImportDeclaration') as [
es.ImportDeclaration[],
es.Statement[]
]
const moduleFunctions: Record<string, ModuleFunctions> = {}

try {
for (const node of importNodes) {
const moduleName = node.source.value
if (typeof moduleName !== 'string') {
throw new Error(`ImportDeclarations should have string sources, got ${moduleName}`)
}

if (!(moduleName in moduleFunctions)) {
context.moduleContexts[moduleName] = {
state: null,
tabs: loadTabs ? loadModuleTabs(moduleName, node) : null
}
moduleFunctions[moduleName] = loadModuleBundle(moduleName, context, node)
}

const functions = moduleFunctions[moduleName]
const environment = currentEnvironment(context)
for (const spec of node.specifiers) {
if (spec.type !== 'ImportSpecifier') {
throw new Error(`Only ImportSpecifiers are supported, got: ${spec.type}`)
const [importNodeMap] = filterImportDeclarations(program)

const environment = currentEnvironment(context)
Object.entries(importNodeMap).forEach(([moduleName, nodes]) => {
initModuleContext(moduleName, context, loadTabs)
const functions = loadModuleBundle(moduleName, context, nodes[0])
for (const node of nodes) {
for (const spec of node.specifiers) {
assert(
spec.type === 'ImportSpecifier',
`Only ImportSpecifiers are supported, got: ${spec.type}`
)

if (checkImports && !(spec.imported.name in functions)) {
throw new UndefinedImportError(spec.imported.name, moduleName, spec)
}

declareIdentifier(context, spec.local.name, node, environment)
defineVariable(context, spec.local.name, functions[spec.imported.name], true, node)
}

if (checkImports && !(spec.imported.name in functions)) {
throw new UndefinedImportError(spec.imported.name, moduleName, node)
}

declareIdentifier(context, spec.local.name, node, environment)
defineVariable(context, spec.local.name, functions[spec.imported.name], true, node)
}
}
})
} catch (error) {
// console.log(error)
handleRuntimeError(context, error)
Expand All @@ -225,7 +213,7 @@
* @returns The corresponding promise.
*/
export function ECEResultPromise(context: Context, value: Value): Promise<Result> {
return new Promise((resolve, reject) => {

Check warning on line 216 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'reject' is defined but never used. Allowed unused args must match /^_/u
if (value instanceof ECEBreak) {
resolve({ status: 'suspended-ec-eval', context })
} else if (value instanceof ECError) {
Expand Down Expand Up @@ -335,7 +323,11 @@
if (hasDeclarations(command) || hasImportDeclarations(command)) {
const environment = createBlockEnvironment(context, 'programEnvironment')
pushEnvironment(context, environment)
evaluateImports(command as unknown as es.Program, context, true, true)
evaluateImports(command as unknown as es.Program, context, {
wrapSourceModules: true,
checkImports: true,
loadTabs: true
})
declareFunctionsAndVariables(context, command, environment)
}

Expand Down Expand Up @@ -451,7 +443,7 @@
}
},

IfStatement: function (command: es.IfStatement, context: Context, agenda: Agenda, stash: Stash) {

Check warning on line 446 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
agenda.push(...reduceConditional(command))
},

Expand Down Expand Up @@ -523,7 +515,7 @@
command: es.ContinueStatement,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 518 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
agenda.push(instr.contInstr(command))
},
Expand All @@ -532,7 +524,7 @@
command: es.BreakStatement,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 527 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
agenda.push(instr.breakInstr(command))
},
Expand Down Expand Up @@ -578,7 +570,7 @@
command: es.MemberExpression,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 573 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
agenda.push(instr.arrAccInstr(command))
agenda.push(command.property)
Expand All @@ -589,7 +581,7 @@
command: es.ConditionalExpression,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 584 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
) {
agenda.push(...reduceConditional(command))
},
Expand Down Expand Up @@ -660,7 +652,7 @@
* Instructions
*/

[InstrType.RESET]: function (command: Instr, context: Context, agenda: Agenda, stash: Stash) {

Check warning on line 655 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
// Keep pushing reset instructions until marker is found.
const cmdNext: AgendaItem | undefined = agenda.pop()
if (cmdNext && (isNode(cmdNext) || cmdNext.instrType !== InstrType.MARKER)) {
Expand Down Expand Up @@ -947,7 +939,7 @@
stash.push(value)
},

[InstrType.CONTINUE]: function (command: Instr, context: Context, agenda: Agenda, stash: Stash) {

Check warning on line 942 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
const next = agenda.pop() as AgendaItem
if (isInstr(next) && next.instrType == InstrType.CONTINUE_MARKER) {
// Encountered continue mark, stop popping
Expand All @@ -962,7 +954,7 @@

[InstrType.CONTINUE_MARKER]: function () {},

[InstrType.BREAK]: function (command: Instr, context: Context, agenda: Agenda, stash: Stash) {

Check warning on line 957 in src/ec-evaluator/interpreter.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

'stash' is defined but never used. Allowed unused args must match /^_/u
const next = agenda.pop() as AgendaItem
if (isInstr(next) && next.instrType == InstrType.BREAK_MARKER) {
// Encountered break mark, stop popping
Expand Down
16 changes: 13 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
FuncDeclWithInferredTypeAnnotation,
ModuleContext,
NodeWithInferredType,
RecursivePartial,
Result,
SourceError,
SVMProgram,
Expand All @@ -31,6 +32,7 @@ import { ECEResultPromise, resumeEvaluate } from './ec-evaluator/interpreter'
import { CannotFindModuleError } from './errors/localImportErrors'
import { validateFilePath } from './localImports/filePaths'
import preprocessFileImports from './localImports/preprocessor'
import type { ImportTransformOptions } from './modules/moduleTypes'
import { getKeywords, getProgramNames, NameDeclaration } from './name-extractor'
import { parse } from './parser/parser'
import { decodeError, decodeValue } from './parser/scheme'
Expand All @@ -56,6 +58,8 @@ export interface IOptions {
isPrelude: boolean
throwInfiniteLoops: boolean
envSteps: number

importOptions: ImportTransformOptions
}

// needed to work on browsers
Expand Down Expand Up @@ -303,7 +307,7 @@ export function getTypeInformation(
export async function runInContext(
code: string,
context: Context,
options: Partial<IOptions> = {}
options: RecursivePartial<IOptions> = {}
): Promise<Result> {
const defaultFilePath = '/default.js'
const files: Partial<Record<string, string>> = {}
Expand All @@ -315,7 +319,7 @@ export async function runFilesInContext(
files: Partial<Record<string, string>>,
entrypointFilePath: string,
context: Context,
options: Partial<IOptions> = {}
options: RecursivePartial<IOptions> = {}
): Promise<Result> {
for (const filePath in files) {
const filePathError = validateFilePath(filePath)
Expand All @@ -340,7 +344,13 @@ export async function runFilesInContext(
if (program === null) {
return resolvedErrorPromise
}
return fullJSRunner(program, context, options)
const fullImportOptions = {
loadTabs: true,
checkImports: false,
wrapSourceModules: false,
...options.importOptions
}
return fullJSRunner(program, context, fullImportOptions)
}

if (context.chapter === Chapter.HTML) {
Expand Down
38 changes: 22 additions & 16 deletions src/infiniteLoops/__tests__/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ function mockFunctionsAndState() {
* Returns the value saved in the code using the builtin 'output'.
* e.g. runWithMock('output(2)') --> 2
*/
function runWithMock(main: string, codeHistory?: string[], builtins: Map<string, any> = new Map()) {
async function runWithMock(
main: string,
codeHistory?: string[],
builtins: Map<string, any> = new Map()
) {
let output = undefined
builtins.set('output', (x: any) => (output = x))
builtins.set('undefined', undefined)
Expand All @@ -53,43 +57,45 @@ function runWithMock(main: string, codeHistory?: string[], builtins: Map<string,
previous = restOfCode as Program[]
}
const [mockFunctions, mockState] = mockFunctionsAndState()
const instrumentedCode = instrument(previous, program as Program, builtins.keys())
const instrumentedCode = await instrument(previous, program as Program, builtins.keys())
const { builtinsId, functionsId, stateId } = InfiniteLoopRuntimeObjectNames
const sandboxedRun = new Function('code', functionsId, stateId, builtinsId, `return eval(code)`)
sandboxedRun(instrumentedCode, mockFunctions, mockState, builtins)
await sandboxedRun(instrumentedCode, mockFunctions, mockState, builtins)
return output
}

test('builtins work', () => {
const main = 'output(2);'
expect(runWithMock(main, [])).toBe(2)
return expect(runWithMock(main, [])).resolves.toBe(2)
})

test('binary and unary expressions work', () => {
expect(runWithMock('output(1+1);', [])).toBe(2)
expect(runWithMock('output(!true);', [])).toBe(false)
return Promise.all([
expect(runWithMock('output(1+1);', [])).resolves.toBe(2),
expect(runWithMock('output(!true);', [])).resolves.toBe(false)
])
})

test('assignment works as expected', () => {
const main = `let x = 2;
let a = [];
a[0] = 3;
output(x+a[0]);`
expect(runWithMock(main)).toBe(5)
return expect(runWithMock(main)).resolves.toBe(5)
})

test('globals from old code accessible', () => {
const main = 'output(z+1);'
const prev = ['const z = w+1;', 'let w = 10;']
expect(runWithMock(main, prev)).toBe(12)
return expect(runWithMock(main, prev)).resolves.toBe(12)
})

test('functions run as expected', () => {
const main = `function f(x,y) {
return x===0?x:f(x-1,y)+y;
}
output(f(5,2));`
expect(runWithMock(main)).toBe(10)
return expect(runWithMock(main)).resolves.toBe(10)
})

test('nested functions run as expected', () => {
Expand All @@ -100,22 +106,22 @@ test('nested functions run as expected', () => {
return x===0?x:f(x-1,y)+y;
}
output(f(5,2));`
expect(runWithMock(main)).toBe(2)
return expect(runWithMock(main)).resolves.toBe(2)
})

test('higher order functions run as expected', () => {
const main = `function run(f, x) {
return f(x+1);
}
output(run(x=>x+1, 1));`
expect(runWithMock(main)).toBe(3)
return expect(runWithMock(main)).resolves.toBe(3)
})

test('loops run as expected', () => {
const main = `let w = 0;
for (let i = w; i < 10; i=i+1) {w = i;}
output(w);`
expect(runWithMock(main)).toBe(9)
return expect(runWithMock(main)).resolves.toBe(9)
})

test('nested loops run as expected', () => {
Expand All @@ -126,13 +132,13 @@ test('nested loops run as expected', () => {
}
}
output(w);`
expect(runWithMock(main)).toBe(100)
return expect(runWithMock(main)).resolves.toBe(100)
})

test('multidimentional arrays work', () => {
const main = `const x = [[1],[2]];
output(x[1] === undefined? undefined: x[1][0]);`
expect(runWithMock(main)).toBe(2)
return expect(runWithMock(main)).resolves.toBe(2)
})

test('if statements work as expected', () => {
Expand All @@ -141,7 +147,7 @@ test('if statements work as expected', () => {
x = x + 1;
} else {}
output(x);`
expect(runWithMock(main)).toBe(2)
return expect(runWithMock(main)).resolves.toBe(2)
})

test('combination of loops and functions run as expected', () => {
Expand All @@ -158,5 +164,5 @@ test('combination of loops and functions run as expected', () => {
w = minus(w,1);
}
output(z);`
expect(runWithMock(main)).toBe(100)
return expect(runWithMock(main)).resolves.toBe(100)
})
Loading
Loading