Skip to content

Commit 06e7b78

Browse files
leeyi45martin-henz
andauthored
Make stdlib available to modules (#1388)
* Add import support to ec evaluator * Add support for import checking * Run format * Fix module loader test * Add tests * Run format * Add require provider * Improve error handling * Make stdlib available * Update module loading and tests * Fix require provider not properly providing js-slang --------- Co-authored-by: Martin Henz <henz@comp.nus.edu.sg>
1 parent 1a87ff3 commit 06e7b78

30 files changed

+708
-156
lines changed

src/__tests__/environment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Program } from 'estree'
22

3-
import { evaluate } from '../interpreter/interpreter'
3+
import { evaluateProgram as evaluate } from '../interpreter/interpreter'
44
import { mockContext } from '../mocks/context'
55
import { parse } from '../parser/parser'
66
import { Chapter } from '../types'
@@ -18,7 +18,7 @@ test('Function params and body identifiers are in different environment', () =>
1818
const context = mockContext(Chapter.SOURCE_4)
1919
context.prelude = null // hide the unneeded prelude
2020
const parsed = parse(code, context)
21-
const it = evaluate(parsed as any as Program, context)
21+
const it = evaluate(parsed as any as Program, context, false, false)
2222
const stepsToComment = 13 // manually counted magic number
2323
for (let i = 0; i < stepsToComment; i += 1) {
2424
it.next()

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Chapter, Language, Variant } from './types'
66
export const DEFAULT_ECMA_VERSION = 6
77
export const ACORN_PARSE_OPTIONS: AcornOptions = { ecmaVersion: DEFAULT_ECMA_VERSION }
88

9+
export const REQUIRE_PROVIDER_ID = 'requireProvider'
910
export const CUT = 'cut' // cut operator for Source 4.3
1011
export const TRY_AGAIN = 'retry' // command for Source 4.3
1112
export const GLOBAL = typeof window === 'undefined' ? global : window

src/ec-evaluator/__tests__/__snapshots__/ec-evaluator-errors.ts.snap

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,19 @@ Object {
792792
}
793793
`;
794794

795+
exports[`Importing unknown variables throws UndefinedImport error: expectParsedError 1`] = `
796+
Object {
797+
"alertResult": Array [],
798+
"code": "import { foo1 } from 'one_module';",
799+
"displayResult": Array [],
800+
"numErrors": 1,
801+
"parsedErrors": "'one_module' does not contain a definition for 'foo1'",
802+
"result": undefined,
803+
"resultStatus": "error",
804+
"visualiseListResult": Array [],
805+
}
806+
`;
807+
795808
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`] = `
796809
Object {
797810
"alertResult": Array [],

src/ec-evaluator/__tests__/__snapshots__/ec-evaluator.ts.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`Imports are properly handled: expectResult 1`] = `
4+
Object {
5+
"alertResult": Array [],
6+
"code": "import { foo } from 'one_module';
7+
foo();",
8+
"displayResult": Array [],
9+
"numErrors": 0,
10+
"parsedErrors": "",
11+
"result": "foo",
12+
"resultStatus": "finished",
13+
"visualiseListResult": Array [],
14+
}
15+
`;
16+
317
exports[`Simple tail call returns work: expectResult 1`] = `
418
Object {
519
"alertResult": Array [],

src/ec-evaluator/__tests__/ec-evaluator-errors.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* tslint:disable:max-line-length */
2+
import * as _ from 'lodash'
3+
24
import { Chapter, Variant } from '../../types'
35
import { stripIndent } from '../../utils/formatters'
46
import {
@@ -8,6 +10,20 @@ import {
810
expectResult
911
} from '../../utils/testing'
1012

13+
jest.spyOn(_, 'memoize').mockImplementation(func => func as any)
14+
15+
const mockXMLHttpRequest = (xhr: Partial<XMLHttpRequest> = {}) => {
16+
const xhrMock: Partial<XMLHttpRequest> = {
17+
open: jest.fn(() => {}),
18+
send: jest.fn(() => {}),
19+
status: 200,
20+
responseText: 'Hello World!',
21+
...xhr
22+
}
23+
jest.spyOn(window, 'XMLHttpRequest').mockImplementationOnce(() => xhrMock as XMLHttpRequest)
24+
return xhrMock
25+
}
26+
1127
const undefinedVariable = stripIndent`
1228
im_undefined;
1329
`
@@ -1000,3 +1016,35 @@ test('Shadowed variables may not be assigned to until declared in the current sc
10001016
optionEC3
10011017
).toMatchInlineSnapshot(`"Line 3: Name variable not declared."`)
10021018
})
1019+
1020+
test('Importing unknown variables throws UndefinedImport error', () => {
1021+
// for getModuleFile
1022+
mockXMLHttpRequest({
1023+
responseText: `{
1024+
"one_module": {
1025+
"tabs": []
1026+
},
1027+
"another_module": {
1028+
"tabs": []
1029+
}
1030+
}`
1031+
})
1032+
1033+
// for bundle body
1034+
mockXMLHttpRequest({
1035+
responseText: `
1036+
require => {
1037+
return {
1038+
foo: () => 'foo',
1039+
}
1040+
}
1041+
`
1042+
})
1043+
1044+
return expectParsedError(
1045+
stripIndent`
1046+
import { foo1 } from 'one_module';
1047+
`,
1048+
optionEC
1049+
).toMatchInlineSnapshot("\"'one_module' does not contain a definition for 'foo1'\"")
1050+
})

src/ec-evaluator/__tests__/ec-evaluator.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ import { Chapter, Variant } from '../../types'
22
import { stripIndent } from '../../utils/formatters'
33
import { expectResult } from '../../utils/testing'
44

5+
// jest.mock('lodash', () => ({
6+
// ...jest.requireActual('lodash'),
7+
// memoize: jest.fn(func => func)
8+
// }))
9+
10+
const mockXMLHttpRequest = (xhr: Partial<XMLHttpRequest> = {}) => {
11+
const xhrMock: Partial<XMLHttpRequest> = {
12+
open: jest.fn(() => {}),
13+
send: jest.fn(() => {}),
14+
status: 200,
15+
responseText: 'Hello World!',
16+
...xhr
17+
}
18+
jest.spyOn(window, 'XMLHttpRequest').mockImplementationOnce(() => xhrMock as XMLHttpRequest)
19+
return xhrMock
20+
}
21+
522
const optionEC = { variant: Variant.EXPLICIT_CONTROL }
623
const optionEC3 = { chapter: Chapter.SOURCE_3, variant: Variant.EXPLICIT_CONTROL }
724
const optionEC4 = { chapter: Chapter.SOURCE_4, variant: Variant.EXPLICIT_CONTROL }
@@ -297,3 +314,36 @@ test('streams can be created and functions with no return statements are still e
297314
optionEC4
298315
).toMatchInlineSnapshot(`false`)
299316
})
317+
318+
test('Imports are properly handled', () => {
319+
// for getModuleFile
320+
mockXMLHttpRequest({
321+
responseText: `{
322+
"one_module": {
323+
"tabs": []
324+
},
325+
"another_module": {
326+
"tabs": []
327+
}
328+
}`
329+
})
330+
331+
// for bundle body
332+
mockXMLHttpRequest({
333+
responseText: `
334+
require => {
335+
return {
336+
foo: () => 'foo',
337+
}
338+
}
339+
`
340+
})
341+
342+
return expectResult(
343+
stripIndent`
344+
import { foo } from 'one_module';
345+
foo();
346+
`,
347+
optionEC
348+
).toEqual('foo')
349+
})

src/ec-evaluator/interpreter.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77

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

1212
import { UNKNOWN_LOCATION } from '../constants'
1313
import * as errors from '../errors/errors'
1414
import { RuntimeSourceError } from '../errors/runtimeSourceError'
1515
import Closure from '../interpreter/closure'
16+
import { UndefinedImportError } from '../modules/errors'
17+
import { loadModuleBundle, loadModuleTabs } from '../modules/moduleLoader'
18+
import { ModuleFunctions } from '../modules/moduleTypes'
1619
import { checkEditorBreakpoints } from '../stdlib/inspector'
1720
import { Context, ContiguousArrayElements, Result, Value } from '../types'
1821
import * as ast from '../utils/astCreator'
@@ -43,6 +46,7 @@ import {
4346
createEnvironment,
4447
currentEnvironment,
4548
declareFunctionsAndVariables,
49+
declareIdentifier,
4650
defineVariable,
4751
getVariable,
4852
handleRuntimeError,
@@ -92,11 +96,18 @@ export class Stash extends Stack<Value> {
9296
export function evaluate(program: es.Program, context: Context): Value {
9397
try {
9498
context.runtime.isRunning = true
95-
context.runtime.agenda = new Agenda(program)
99+
100+
const nonImportNodes = evaluateImports(program, context, true, true)
101+
102+
context.runtime.agenda = new Agenda({
103+
...program,
104+
body: nonImportNodes
105+
})
96106
context.runtime.stash = new Stash()
97107
return runECEMachine(context, context.runtime.agenda, context.runtime.stash)
98108
} catch (error) {
99-
return new ECError()
109+
// console.error('ecerror:', error)
110+
return new ECError(error)
100111
} finally {
101112
context.runtime.isRunning = false
102113
}
@@ -115,12 +126,63 @@ export function resumeEvaluate(context: Context) {
115126
context.runtime.isRunning = true
116127
return runECEMachine(context, context.runtime.agenda!, context.runtime.stash!)
117128
} catch (error) {
118-
return new ECError()
129+
return new ECError(error)
119130
} finally {
120131
context.runtime.isRunning = false
121132
}
122133
}
123134

135+
function evaluateImports(
136+
program: es.Program,
137+
context: Context,
138+
loadTabs: boolean,
139+
checkImports: boolean
140+
) {
141+
const [importNodes, otherNodes] = partition(
142+
program.body,
143+
({ type }) => type === 'ImportDeclaration'
144+
) as [es.ImportDeclaration[], es.Statement[]]
145+
146+
const moduleFunctions: Record<string, ModuleFunctions> = {}
147+
148+
try {
149+
for (const node of importNodes) {
150+
const moduleName = node.source.value
151+
if (typeof moduleName !== 'string') {
152+
throw new Error(`ImportDeclarations should have string sources, got ${moduleName}`)
153+
}
154+
155+
if (!(moduleName in moduleFunctions)) {
156+
context.moduleContexts[moduleName] = {
157+
state: null,
158+
tabs: loadTabs ? loadModuleTabs(moduleName, node) : null
159+
}
160+
moduleFunctions[moduleName] = loadModuleBundle(moduleName, context, node)
161+
}
162+
163+
const functions = moduleFunctions[moduleName]
164+
const environment = currentEnvironment(context)
165+
for (const spec of node.specifiers) {
166+
if (spec.type !== 'ImportSpecifier') {
167+
throw new Error(`Only ImportSpecifiers are supported, got: ${spec.type}`)
168+
}
169+
170+
if (checkImports && !(spec.imported.name in functions)) {
171+
throw new UndefinedImportError(spec.imported.name, moduleName, node)
172+
}
173+
174+
declareIdentifier(context, spec.local.name, node, environment)
175+
defineVariable(context, spec.local.name, functions[spec.imported.name], true, node)
176+
}
177+
}
178+
} catch (error) {
179+
// console.log(error)
180+
handleRuntimeError(context, error)
181+
}
182+
183+
return otherNodes
184+
}
185+
124186
/**
125187
* Function that returns the appropriate Promise<Result> given the output of ec evaluating, depending
126188
* on whether the program is finished evaluating, ran into a breakpoint or ran into an error.
@@ -339,6 +401,9 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = {
339401
) {
340402
agenda.push(instr.breakInstr())
341403
},
404+
ImportDeclaration: function () {
405+
throw new Error('Import Declarations should already have been removed.')
406+
},
342407

343408
/**
344409
* Expressions

src/ec-evaluator/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,6 @@ export class ECEBreak {}
105105

106106
// Special value that cannot be found on the stash so is safe to be used
107107
// as an indicator of an error from running the ECE machine
108-
export class ECError {}
108+
export class ECError {
109+
constructor(public readonly error: any) {}
110+
}

src/ec-evaluator/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export const createBlockEnvironment = (
205205

206206
const DECLARED_BUT_NOT_YET_ASSIGNED = Symbol('Used to implement hoisting')
207207

208-
function declareIdentifier(
208+
export function declareIdentifier(
209209
context: Context,
210210
name: string,
211211
node: es.Node,
@@ -259,7 +259,7 @@ export function defineVariable(
259259
name: string,
260260
value: Value,
261261
constant = false,
262-
node: es.VariableDeclaration
262+
node: es.VariableDeclaration | es.ImportDeclaration
263263
) {
264264
const environment = currentEnvironment(context)
265265

src/errors/moduleErrors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ModuleNotFoundError extends RuntimeSourceError {
3636
}
3737

3838
export class ModuleInternalError extends RuntimeSourceError {
39-
constructor(public moduleName: string, node?: es.Node) {
39+
constructor(public moduleName: string, public error?: any, node?: es.Node) {
4040
super(node)
4141
}
4242

0 commit comments

Comments
 (0)