Skip to content

Commit 56252ea

Browse files
authoredApr 13, 2020
Module Syntax Support (#536)
* Add primary module support Add primary module support for browsers. * format moduleLoader.js * support imports in interpreter and transpiler * fix minor problems * fix minor problems * fix that imports scanning would break variables * fix library cannot use global object * fix libraries using global object * add error handling * remove trashes * fix format * Update package.json and yarn.lock Update package.json and yarn.lock * add tests for transformImportDeclarations * file format * Add error handling for modules * format file * change term 'hoist' to 'declare' * file format * add tests for loadIIFEModule * add tests for single import declaration * change 'loadIIFEModuleText' to 'loadModuleText'
1 parent b81bd34 commit 56252ea

File tree

10 files changed

+1701
-1310
lines changed

10 files changed

+1701
-1310
lines changed
 

‎.vscode/launch.json

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "Launch Source REPL",
11+
"skipFiles": [
12+
"<node_internals>/**"
13+
],
14+
"program": "${workspaceFolder}/dist/repl/repl.js",
15+
"console": "integratedTerminal",
16+
"preLaunchTask": "tsc: build - tsconfig.json",
17+
"outFiles": [
18+
"${workspaceFolder}/dist/**/*.js"
19+
]
20+
},
21+
{
22+
"type": "node",
23+
"request": "launch",
24+
"name": "Launch Source REPL for test.js",
25+
"skipFiles": [
26+
"<node_internals>/**"
27+
],
28+
"program": "${workspaceFolder}/dist/repl/repl.js",
29+
"args": ["~/test.js"],
30+
"console": "integratedTerminal",
31+
"preLaunchTask": "tsc: build - tsconfig.json",
32+
"outFiles": [
33+
"${workspaceFolder}/dist/**/*.js"
34+
]
35+
}
36+
]
37+
}

‎.vscode/settings.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"restructuredtext.confPath": "",
3+
"restructuredtext.linter.disabled": true
4+
}

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"jest-html-reporter": "^2.8.2",
2626
"lodash": "^4.17.13",
2727
"node-getopt": "^0.3.2",
28-
"source-map": "^0.7.3"
28+
"source-map": "^0.7.3",
29+
"xmlhttprequest-ts": "^1.0.1"
2930
},
3031
"main": "dist/index",
3132
"types": "dist/index",

‎src/errors/errors.ts

+32
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,38 @@ export class InterruptedError extends RuntimeSourceError {
2020
}
2121
}
2222

23+
export class ModuleNotFound extends RuntimeSourceError {
24+
constructor(public moduleName: string, node?: es.Node) {
25+
super(node)
26+
}
27+
28+
public explain() {
29+
return `Module "${this.moduleName}" not found.`
30+
}
31+
32+
public elaborate() {
33+
return `
34+
You should check your Internet connection, and ensure you have used the correct module path.
35+
`
36+
}
37+
}
38+
39+
export class ModuleInternalError extends RuntimeSourceError {
40+
constructor(public moduleName: string, node?: es.Node) {
41+
super(node)
42+
}
43+
44+
public explain() {
45+
return `Error(s) occured when executing the module "${this.moduleName}".`
46+
}
47+
48+
public elaborate() {
49+
return `
50+
You may need to contact with the author for this module to fix this error.
51+
`
52+
}
53+
}
54+
2355
export class ExceptionError implements SourceError {
2456
public type = ErrorType.RUNTIME
2557
public severity = ErrorSeverity.ERROR

‎src/interpreter/interpreter.ts

+32-17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { conditionalExpression, literal, primitive } from '../utils/astCreator'
99
import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators'
1010
import * as rttc from '../utils/rttc'
1111
import Closure from './closure'
12+
import { loadIIFEModule } from '../modules/moduleLoader'
1213

1314
class BreakValue {}
1415

@@ -96,9 +97,9 @@ const handleRuntimeError = (context: Context, error: RuntimeSourceError): never
9697
throw error
9798
}
9899

99-
const HOISTED_BUT_NOT_YET_ASSIGNED = Symbol('Used to implement hoisting')
100+
const DECLARED_BUT_NOT_YET_ASSIGNED = Symbol('Used to implement hoisting')
100101

101-
function hoistIdentifier(context: Context, name: string, node: es.Node) {
102+
function declareIdentifier(context: Context, name: string, node: es.Node) {
102103
const environment = currentEnvironment(context)
103104
if (environment.head.hasOwnProperty(name)) {
104105
const descriptors = Object.getOwnPropertyDescriptors(environment.head)
@@ -108,27 +109,30 @@ function hoistIdentifier(context: Context, name: string, node: es.Node) {
108109
new errors.VariableRedeclaration(node, name, descriptors[name].writable)
109110
)
110111
}
111-
environment.head[name] = HOISTED_BUT_NOT_YET_ASSIGNED
112+
environment.head[name] = DECLARED_BUT_NOT_YET_ASSIGNED
112113
return environment
113114
}
114115

115-
function hoistVariableDeclarations(context: Context, node: es.VariableDeclaration) {
116+
function declareVariables(context: Context, node: es.VariableDeclaration) {
116117
for (const declaration of node.declarations) {
117-
hoistIdentifier(context, (declaration.id as es.Identifier).name, node)
118+
declareIdentifier(context, (declaration.id as es.Identifier).name, node)
118119
}
119120
}
120121

121-
function hoistFunctionsAndVariableDeclarationsIdentifiers(
122-
context: Context,
123-
node: es.BlockStatement
124-
) {
122+
function declareImports(context: Context, node: es.ImportDeclaration) {
123+
for (const declaration of node.specifiers) {
124+
declareIdentifier(context, declaration.local.name, node)
125+
}
126+
}
127+
128+
function declareFunctionsAndVariables(context: Context, node: es.BlockStatement) {
125129
for (const statement of node.body) {
126130
switch (statement.type) {
127131
case 'VariableDeclaration':
128-
hoistVariableDeclarations(context, statement)
132+
declareVariables(context, statement)
129133
break
130134
case 'FunctionDeclaration':
131-
hoistIdentifier(context, (statement.id as es.Identifier).name, statement)
135+
declareIdentifier(context, (statement.id as es.Identifier).name, statement)
132136
break
133137
}
134138
}
@@ -137,7 +141,7 @@ function hoistFunctionsAndVariableDeclarationsIdentifiers(
137141
function defineVariable(context: Context, name: string, value: Value, constant = false) {
138142
const environment = context.runtime.environments[0]
139143

140-
if (environment.head[name] !== HOISTED_BUT_NOT_YET_ASSIGNED) {
144+
if (environment.head[name] !== DECLARED_BUT_NOT_YET_ASSIGNED) {
141145
return handleRuntimeError(
142146
context,
143147
new errors.VariableRedeclaration(context.runtime.nodes[0]!, name, !constant)
@@ -176,7 +180,7 @@ const getVariable = (context: Context, name: string) => {
176180
let environment: Environment | null = context.runtime.environments[0]
177181
while (environment) {
178182
if (environment.head.hasOwnProperty(name)) {
179-
if (environment.head[name] === HOISTED_BUT_NOT_YET_ASSIGNED) {
183+
if (environment.head[name] === DECLARED_BUT_NOT_YET_ASSIGNED) {
180184
return handleRuntimeError(
181185
context,
182186
new errors.UnassignedVariable(name, context.runtime.nodes[0])
@@ -195,7 +199,7 @@ const setVariable = (context: Context, name: string, value: any) => {
195199
let environment: Environment | null = context.runtime.environments[0]
196200
while (environment) {
197201
if (environment.head.hasOwnProperty(name)) {
198-
if (environment.head[name] === HOISTED_BUT_NOT_YET_ASSIGNED) {
202+
if (environment.head[name] === DECLARED_BUT_NOT_YET_ASSIGNED) {
199203
break
200204
}
201205
const descriptors = Object.getOwnPropertyDescriptors(environment.head)
@@ -266,7 +270,7 @@ function* reduceIf(
266270
export type Evaluator<T extends es.Node> = (node: T, context: Context) => IterableIterator<Value>
267271

268272
function* evaluateBlockSatement(context: Context, node: es.BlockStatement) {
269-
hoistFunctionsAndVariableDeclarationsIdentifiers(context, node)
273+
declareFunctionsAndVariables(context, node)
270274
let result
271275
for (const statement of node.body) {
272276
result = yield* evaluate(statement, context)
@@ -412,7 +416,7 @@ export const evaluators: { [nodeType: string]: Evaluator<es.Node> } = {
412416
const testNode = node.test!
413417
const updateNode = node.update!
414418
if (initNode.type === 'VariableDeclaration') {
415-
hoistVariableDeclarations(context, initNode)
419+
declareVariables(context, initNode)
416420
}
417421
yield* actualValue(initNode, context)
418422

@@ -427,7 +431,7 @@ export const evaluators: { [nodeType: string]: Evaluator<es.Node> } = {
427431
pushEnvironment(context, environment)
428432
for (const name in loopEnvironment.head) {
429433
if (loopEnvironment.head.hasOwnProperty(name)) {
430-
hoistIdentifier(context, name, node)
434+
declareIdentifier(context, name, node)
431435
defineVariable(context, name, loopEnvironment.head[name], true)
432436
}
433437
}
@@ -600,6 +604,17 @@ export const evaluators: { [nodeType: string]: Evaluator<es.Node> } = {
600604
return result
601605
},
602606

607+
ImportDeclaration: function*(node: es.ImportDeclaration, context: Context) {
608+
const moduleName = node.source.value as string
609+
const neededSymbols = node.specifiers.map(spec => spec.local.name)
610+
const module = loadIIFEModule(moduleName)
611+
declareImports(context, node)
612+
for (const name of neededSymbols) {
613+
defineVariable(context, name, module[name], true);
614+
}
615+
return undefined
616+
},
617+
603618
Program: function*(node: es.BlockStatement, context: Context) {
604619
context.numberOfOuterEnvironments += 1
605620
const environment = createBlockEnvironment(context, 'programEnvironment')

‎src/modules/__tests__/moduleLoader.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { loadModuleText, loadIIFEModule } from '../moduleLoader'
6+
import { ModuleNotFound, ModuleInternalError } from '../../errors/errors'
7+
import { stripIndent } from '../../utils/formatters'
8+
9+
test('Try loading a non-existing module', () => {
10+
const moduleName = '_non_existing_dir/_non_existing_file'
11+
expect(() => loadModuleText(moduleName)).toThrow(ModuleNotFound)
12+
})
13+
14+
test('Try executing a wrongly implemented module', () => {
15+
// A module in wrong format
16+
const path = '_mock_dir/_mock_file'
17+
const wrongModuleText = stripIndent`
18+
export function es6_function(params) {}
19+
`
20+
expect(() => loadIIFEModule(path, wrongModuleText)).toThrow(ModuleInternalError)
21+
})

‎src/modules/moduleLoader.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ModuleNotFound, ModuleInternalError } from '../errors/errors'
2+
import { XMLHttpRequest as NodeXMLHttpRequest } from 'xmlhttprequest-ts'
3+
const HttpRequest = typeof window === 'undefined' ? NodeXMLHttpRequest : XMLHttpRequest
4+
5+
// TODO: Change this URL to actual Backend URL
6+
const BACKEND_STATIC_URL = 'http://ec2-54-169-81-133.ap-southeast-1.compute.amazonaws.com/static'
7+
8+
export function loadModuleText(path: string) {
9+
const scriptPath = `${BACKEND_STATIC_URL}/${path}.js`
10+
const req = new HttpRequest()
11+
req.open('GET', scriptPath, false)
12+
req.send(null)
13+
if (req.status !== 200 && req.status !== 304) {
14+
throw new ModuleNotFound(`module ${path} not found.`)
15+
}
16+
return req.responseText
17+
}
18+
19+
/* tslint:disable */
20+
export function loadIIFEModule(path: string, moduleText?: string) {
21+
try {
22+
if (moduleText === undefined) {
23+
moduleText = loadModuleText(path)
24+
}
25+
return eval(moduleText) as object
26+
} catch (_error) {
27+
throw new ModuleInternalError(path)
28+
}
29+
}

‎src/transpiler/__tests__/modules.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { transformSingleImportDeclaration, transformImportDeclarations } from '../transpiler'
2+
import { stripIndent } from '../../utils/formatters'
3+
import { parse } from '../../parser/parser'
4+
import { mockContext } from '../../mocks/context'
5+
import { ImportDeclaration, Identifier } from 'estree'
6+
7+
test('Transform single import decalration', () => {
8+
const code = `import { foo, bar } from "test/one_module";`
9+
const context = mockContext(4)
10+
const program = parse(code, context)!
11+
const result = transformSingleImportDeclaration(123, program.body[0] as ImportDeclaration)
12+
const names = result.map(decl => (decl.declarations[0].id as Identifier).name)
13+
expect(names[0]).toStrictEqual('foo')
14+
expect(names[1]).toStrictEqual('bar')
15+
})
16+
17+
test('Transform import decalrations variable decalarations', () => {
18+
const code = stripIndent`
19+
import { foo } from "test/one_module";
20+
import { bar } from "test/another_module";
21+
foo(bar);
22+
`
23+
const context = mockContext(4)
24+
const program = parse(code, context)!
25+
transformImportDeclarations(program)
26+
expect(program.body[0].type).toBe('VariableDeclaration')
27+
expect(program.body[1].type).toBe('VariableDeclaration')
28+
})

0 commit comments

Comments
 (0)
Please sign in to comment.