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

Further Enhancements to the Module System #1428

Closed
wants to merge 55 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
16ba5ad
Update configuration files
leeyi45 Jun 22, 2023
c1a51dd
Add async module loading code and tests
leeyi45 Jun 22, 2023
f8e3311
Add general utils
leeyi45 Jun 22, 2023
40b1eb3
Add helper types
leeyi45 Jun 22, 2023
d639512
Move preprocessor to modules folder
leeyi45 Jun 22, 2023
6d81ad4
Relocate ast creator to new utils folder
leeyi45 Jun 22, 2023
8acfd79
Add AST utils and relocate walkers
leeyi45 Jun 22, 2023
b00153e
Clean version of async-imports3
leeyi45 Jun 22, 2023
a1f47f4
Ran format
leeyi45 Jun 22, 2023
ef8d903
Update error tests
leeyi45 Jun 22, 2023
150e94e
Ran format
leeyi45 Jun 22, 2023
509be3b
Update snapshot
leeyi45 Jun 22, 2023
d565099
Remove unnecessary code
leeyi45 Jun 22, 2023
92d7b48
Clean up ec-evaluator implementation for imports
leeyi45 Jun 23, 2023
d493ffa
Fix accumulate working in the wrong direction
leeyi45 Jun 23, 2023
3ba38ef
Ran format
leeyi45 Jun 23, 2023
813cceb
Update list test to take initial argument into account
leeyi45 Jun 23, 2023
2a2ac1f
Add missing tests
leeyi45 Jun 23, 2023
ba1cc1b
Ran format
leeyi45 Jun 23, 2023
532a944
Ran format
leeyi45 Jun 23, 2023
fbbb690
Add tests for length()
leeyi45 Jun 28, 2023
24ec58b
Merge commit '8618e26e47595bdeb609857d7d3265cde09beb37' into async-im…
leeyi45 Jun 28, 2023
43481bb
Ensure that moduleLoaderAsync properly handles fetch type errors
leeyi45 Jun 28, 2023
9ed1e54
Fix broken error handling
leeyi45 Jun 28, 2023
0380f5e
Add test to ensure that TypeErrors thrown by fetch are caught
leeyi45 Jun 28, 2023
d67af01
Fix fetch not timing out when modules server is unreachable
leeyi45 Jul 14, 2023
bc6a53f
Ran format
leeyi45 Jul 14, 2023
fb49b56
fix(deps): update all non-major dependencies
renovate[bot] Jul 19, 2023
bd4b132
Merge from master
leeyi45 Jul 19, 2023
1e05b13
Update @types/node
leeyi45 Jul 19, 2023
d1e5800
Ran format
leeyi45 Jul 19, 2023
937bae9
Update stepper
leeyi45 Jul 29, 2023
d330c39
Move module mocking to individual tests
leeyi45 Jul 29, 2023
d06a2ca
Refactor ast utils
leeyi45 Jul 30, 2023
f66472d
Fix broken path
leeyi45 Jul 30, 2023
157cd3b
Add no rexport declaration rule
leeyi45 Jul 30, 2023
545f558
fix(deps): update all non-major dependencies
renovate[bot] Jul 31, 2023
fd901b9
Add no reexport declaration rule
leeyi45 Aug 1, 2023
8553c76
Ensure reexport rule is not enforced for FullJS
leeyi45 Aug 1, 2023
f4fc2e2
Update code to use type guards
leeyi45 Aug 1, 2023
2b7eb11
Ran format
leeyi45 Aug 1, 2023
cdbac82
Revert node types change temporarily
leeyi45 Aug 1, 2023
a3d9c4d
Merge commit '545f558853f44e377aca8822074ea1730f42921e' into async-im…
leeyi45 Aug 1, 2023
59dbf48
Ran format
leeyi45 Aug 1, 2023
dfb5882
Update stepper and tests for async
leeyi45 Aug 2, 2023
0dc35cc
Add tests for ArrayMap
leeyi45 Aug 2, 2023
5f8f68e
Fix broken tests
leeyi45 Aug 2, 2023
f264219
Update code to use ArrayMap
leeyi45 Aug 2, 2023
b05f072
Update node types
leeyi45 Aug 2, 2023
a8f3951
Update node types
leeyi45 Aug 2, 2023
657ab8c
Remove global mocks entirely
leeyi45 Aug 2, 2023
9913230
Fix tests not working with wrap-ansi
leeyi45 Aug 2, 2023
288af5d
Ran format
leeyi45 Aug 2, 2023
703da37
Merge branch 'master' into async-imports-clean
martin-henz Aug 7, 2023
1d75fa2
Merge branch 'master' into async-imports-clean
martin-henz Aug 7, 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: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"presets": ["es2015", "stage-2"]
"presets": ["@babel/preset-env"]
}
5 changes: 5 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"ignorePatterns": [
"**/__tests__/**",
"**/__mocks__/**",
"jest.setup.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules
dist/
.idea/
coverage/
yarn-error.log

# emacs backup files
*~
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
"dependencies": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump @types/node (dev dep) to 20.4.4 to solve CI error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my system, updating @types/node produces this error when running yarn test:

Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/leeyi45/source/source_academy/js-slang/node_modules/wrap-ansi/index.js from /Users/leeyi45/source/source_academy/js-slang/node_modules/cliui/build/index.cjs not supported.
Instead change the require of index.js in /Users/leeyi45/source/source_academy/js-slang/node_modules/cliui/build/index.cjs to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/Users/leeyi45/source/source_academy/js-slang/node_modules/cliui/build/index.cjs:293:14)
    at Object.<anonymous> (/Users/leeyi45/source/source_academy/js-slang/node_modules/yargs/build/index.cjs:1:60678)
    at Object.<anonymous> (/Users/leeyi45/source/source_academy/js-slang/node_modules/yargs/index.cjs:5:30)
    at _yargs (/Users/leeyi45/source/source_academy/js-slang/node_modules/jest-cli/build/run.js:30:39)
    at buildArgv (/Users/leeyi45/source/source_academy/js-slang/node_modules/jest-cli/build/run.js:143:26)
    at Object.run (/Users/leeyi45/source/source_academy/js-slang/node_modules/jest-cli/build/run.js:118:24)
    at Object.<anonymous> (/Users/leeyi45/source/source_academy/js-slang/node_modules/jest-cli/bin/jest.js:16:17)
    at Object.<anonymous> (/Users/leeyi45/source/source_academy/js-slang/node_modules/jest/bin/jest.js:12:3)

"@babel/parser": "^7.19.4",
"@joeychenofficial/alt-ergo-modified": "^2.4.0",
"@ts-morph/bootstrap": "^0.18.0",
"@ts-morph/bootstrap": "^0.20.0",
"@types/estree": "0.0.52",
"acorn": "^8.8.2",
"acorn": "^8.10.0",
"acorn-class-fields": "^1.0.0",
"acorn-loose": "^8.0.0",
"acorn-walk": "^8.0.0",
Expand All @@ -44,7 +44,7 @@
"js-base64": "^3.7.5",
"lodash": "^4.17.20",
"node-getopt": "^0.3.2",
"source-map": "0.7.3",
"source-map": "0.7.4",
"xmlhttprequest-ts": "^1.0.1"
},
"scripts": {
Expand All @@ -67,7 +67,7 @@
"@types/jest": "^29.0.0",
"@types/lodash.assignin": "^4.2.6",
"@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^17.0.5",
"@types/node": "^20.4.5",
"@types/offscreencanvas": "^2019.7.0",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
Expand Down
1,111 changes: 606 additions & 505 deletions sicp_publish/yarn.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/__tests__/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ test('Function params and body identifiers are in different environment', () =>
const context = mockContext(Chapter.SOURCE_4)
context.prelude = null // hide the unneeded prelude
const parsed = parse(code, context)
const it = evaluate(parsed as any as Program, context, false, false)
const it = evaluate(parsed as any as Program, context, {
loadTabs: false,
wrapModules: false
})
const stepsToComment = 13 // manually counted magic number
for (let i = 0; i < stepsToComment; i += 1) {
it.next()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -792,19 +792,6 @@ Object {
}
`;

exports[`Importing unknown variables throws UndefinedImport error: expectParsedError 1`] = `
Object {
"alertResult": Array [],
"code": "import { foo1 } from 'one_module';",
"displayResult": Array [],
"numErrors": 1,
"parsedErrors": "'one_module' does not contain a definition for 'foo1'",
"result": undefined,
"resultStatus": "error",
"visualiseListResult": Array [],
}
`;

exports[`In a block, every going-to-be-defined variable in the block cannot be accessed until it has been defined in the block.: expectParsedError 1`] = `
Object {
"alertResult": Array [],
Expand Down
48 changes: 0 additions & 48 deletions src/ec-evaluator/__tests__/ec-evaluator-errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/* tslint:disable:max-line-length */
import * as _ from 'lodash'

import { Chapter, Variant } from '../../types'
import { stripIndent } from '../../utils/formatters'
import {
Expand All @@ -10,20 +8,6 @@ import {
expectResult
} from '../../utils/testing'

jest.spyOn(_, 'memoize').mockImplementation(func => func as any)

const mockXMLHttpRequest = (xhr: Partial<XMLHttpRequest> = {}) => {
const xhrMock: Partial<XMLHttpRequest> = {
open: jest.fn(() => {}),
send: jest.fn(() => {}),
status: 200,
responseText: 'Hello World!',
...xhr
}
jest.spyOn(window, 'XMLHttpRequest').mockImplementationOnce(() => xhrMock as XMLHttpRequest)
return xhrMock
}

const undefinedVariable = stripIndent`
im_undefined;
`
Expand Down Expand Up @@ -1016,35 +1000,3 @@ test('Shadowed variables may not be assigned to until declared in the current sc
optionEC3
).toMatchInlineSnapshot(`"Line 3: Name variable not declared."`)
})

test('Importing unknown variables throws UndefinedImport error', () => {
// for getModuleFile
mockXMLHttpRequest({
responseText: `{
"one_module": {
"tabs": []
},
"another_module": {
"tabs": []
}
}`
})

// for bundle body
mockXMLHttpRequest({
responseText: `
require => {
return {
foo: () => 'foo',
}
}
`
})

return expectParsedError(
stripIndent`
import { foo1 } from 'one_module';
`,
optionEC
).toMatchInlineSnapshot("\"'one_module' does not contain a definition for 'foo1'\"")
})
41 changes: 2 additions & 39 deletions src/ec-evaluator/__tests__/ec-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,8 @@ import { Chapter, Variant } from '../../types'
import { stripIndent } from '../../utils/formatters'
import { expectResult } from '../../utils/testing'

// jest.mock('lodash', () => ({
// ...jest.requireActual('lodash'),
// memoize: jest.fn(func => func)
// }))

const mockXMLHttpRequest = (xhr: Partial<XMLHttpRequest> = {}) => {
const xhrMock: Partial<XMLHttpRequest> = {
open: jest.fn(() => {}),
send: jest.fn(() => {}),
status: 200,
responseText: 'Hello World!',
...xhr
}
jest.spyOn(window, 'XMLHttpRequest').mockImplementationOnce(() => xhrMock as XMLHttpRequest)
return xhrMock
}
jest.mock('../../modules/moduleLoaderAsync')
jest.mock('../../modules/moduleLoader')

const optionEC = { variant: Variant.EXPLICIT_CONTROL }
const optionEC3 = { chapter: Chapter.SOURCE_3, variant: Variant.EXPLICIT_CONTROL }
Expand Down Expand Up @@ -316,29 +302,6 @@ test('streams can be created and functions with no return statements are still e
})

test('Imports are properly handled', () => {
// for getModuleFile
mockXMLHttpRequest({
responseText: `{
"one_module": {
"tabs": []
},
"another_module": {
"tabs": []
}
}`
})

// for bundle body
mockXMLHttpRequest({
responseText: `
require => {
return {
foo: () => 'foo',
}
}
`
})

return expectResult(
stripIndent`
import { foo } from 'one_module';
Expand Down
143 changes: 91 additions & 52 deletions src/ec-evaluator/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
*/

/* 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 { loadModuleBundle } from '../modules/moduleLoader'
import type { ImportTransformOptions } from '../modules/moduleTypes'
import { initModuleContext } from '../modules/utils'
import { checkEditorBreakpoints } from '../stdlib/inspector'
import { Context, ContiguousArrayElements, Result, Value } from '../types'
import * as ast from '../utils/astCreator'
import type { Context, ContiguousArrayElements, Result, Value } from '../types'
import { arrayMapFrom } from '../utils/arrayMap'
import assert from '../utils/assert'
import * as ast from '../utils/ast/astCreator'
import { isImportDeclaration } from '../utils/ast/typeGuards'
import type * as es from '../utils/ast/types'
import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators'
import * as rttc from '../utils/rttc'
import * as instr from './instrCreator'
Expand Down Expand Up @@ -129,7 +132,11 @@
* @param context The context to evaluate the program in.
* @returns The result of running the ECE machine.
*/
export function evaluate(program: es.Program, context: Context, options: IOptions): Value {
export async function evaluate(
program: es.Program,
context: Context,
options: IOptions
): Promise<Value> {
try {
context.runtime.isRunning = true
context.runtime.agenda = new Agenda(program)
Expand Down Expand Up @@ -172,47 +179,77 @@
function evaluateImports(
program: es.Program,
context: Context,
loadTabs: boolean,
checkImports: boolean
{ loadTabs, wrapModules }: ImportTransformOptions
) {
const [importNodes] = partition(program.body, ({ type }) => type === 'ImportDeclaration') as [
es.ImportDeclaration[],
es.Statement[]
]
const moduleFunctions: Record<string, ModuleFunctions> = {}
const importNodes = program.body.filter(isImportDeclaration)

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}`)
}
const importNodeMap = arrayMapFrom(
importNodes.map(node => {
const moduleName = node.source.value
assert(
typeof moduleName === 'string',
`ImportDeclarations should have string sources, got ${moduleName}`
)
return [moduleName, node]
})
)

if (!(moduleName in moduleFunctions)) {
context.moduleContexts[moduleName] = {
state: null,
tabs: loadTabs ? loadModuleTabs(moduleName, node) : null
const environment = currentEnvironment(context)
importNodeMap.entries().forEach(([moduleName, nodes]) => {
initModuleContext(moduleName, context, loadTabs, nodes[0])
const functions = loadModuleBundle(moduleName, context, wrapModules, nodes[0])
for (const node of nodes) {
for (const spec of node.specifiers) {
let importedName: string
switch (spec.type) {
case 'ImportSpecifier': {
importedName = spec.imported.name
break
}
case 'ImportDefaultSpecifier': {
importedName = 'default'
break
}
case 'ImportNamespaceSpecifier': {
throw new Error('Namespace imports are not supported!')
}
}

declareIdentifier(context, spec.local.name, node, environment)
defineVariable(context, spec.local.name, functions[importedName], true, node)
}
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}`)
}

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)
}
}
// await importNodeMap.forEachAsync(async (moduleName, nodes) => {
// await initModuleContextAsync(moduleName, context, loadTabs, nodes[0])
// const functions = await loadModuleBundleAsync(moduleName, context, wrapModules, nodes[0])

// for (const node of nodes) {
// for (const spec of node.specifiers) {
// let importedName: string
// switch (spec.type) {
// case 'ImportSpecifier': {
// importedName = spec.imported.name
// break
// }
// case 'ImportDefaultSpecifier': {
// importedName = 'default'
// break
// }
// case 'ImportNamespaceSpecifier': {
// throw new Error('Namespace imports are not supported!')
// }
// }

// declareIdentifier(context, spec.local.name, node, environment)
// defineVariable(context, spec.local.name, functions[importedName], true, node)
// }
// }
// })
} catch (error) {
// console.log(error)
// console.error(error)
handleRuntimeError(context, error)
}
}
Expand All @@ -224,16 +261,15 @@
* @param value The value of ec evaluating the program.
* @returns The corresponding promise.
*/
export function ECEResultPromise(context: Context, value: Value): Promise<Result> {
return new Promise((resolve, reject) => {
if (value instanceof ECEBreak) {
resolve({ status: 'suspended-ec-eval', context })
} else if (value instanceof ECError) {
resolve({ status: 'error' })
} else {
resolve({ status: 'finished', context, value })
}
})
export async function ECEResultPromise(context: Context, promise: Promise<Value>): Promise<Result> {
const value = await promise
if (value instanceof ECEBreak) {
return { status: 'suspended-ec-eval', context }
} else if (value instanceof ECError) {
return { status: 'error' }
} else {
return { status: 'finished', context, value }
}
}

/**
Expand Down Expand Up @@ -335,7 +371,10 @@
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, {
loadTabs: true,
wrapModules: true
})
declareFunctionsAndVariables(context, command, environment)
}

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

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

Check warning on line 493 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 +562,7 @@
command: es.ContinueStatement,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 565 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 +571,7 @@
command: es.BreakStatement,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 574 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 +617,7 @@
command: es.MemberExpression,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 620 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 +628,7 @@
command: es.ConditionalExpression,
context: Context,
agenda: Agenda,
stash: Stash

Check warning on line 631 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 +699,7 @@
* Instructions
*/

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

Check warning on line 702 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 +986,7 @@
stash.push(value)
},

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

Check warning on line 989 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 +1001,7 @@

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

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

Check warning on line 1004 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
Loading
Loading