diff --git a/src/ec-evaluator/interpreter.ts b/src/ec-evaluator/interpreter.ts index 739654e0e..7b77585d3 100644 --- a/src/ec-evaluator/interpreter.ts +++ b/src/ec-evaluator/interpreter.ts @@ -194,7 +194,9 @@ function evaluateImports( } declareIdentifier(context, spec.local.name, node, environment) - defineVariable(context, spec.local.name, functions[spec.imported.name], true, node) + const importedObj = functions[spec.imported.name] + Object.defineProperty(importedObj, 'name', { value: spec.local.name }) + defineVariable(context, spec.local.name, importedObj, true, node) } } }) diff --git a/src/infiniteLoops/instrument.ts b/src/infiniteLoops/instrument.ts index 6b3af9749..8cc755e14 100644 --- a/src/infiniteLoops/instrument.ts +++ b/src/infiniteLoops/instrument.ts @@ -587,7 +587,9 @@ async function handleImports(programs: es.Program[]): Promise<[string, string[]] } ) program.body = (importsToAdd as es.Program['body']).concat(otherNodes) - const importedNames = importsToAdd.flatMap(node => + const importedNames = ( + importsToAdd.filter(node => node.type === 'VariableDeclaration') as es.VariableDeclaration[] + ).flatMap(node => node.declarations.map( decl => ((decl.init as es.MemberExpression).object as es.Identifier).name ) diff --git a/src/infiniteLoops/runtime.ts b/src/infiniteLoops/runtime.ts index 83783e251..1d767460a 100644 --- a/src/infiniteLoops/runtime.ts +++ b/src/infiniteLoops/runtime.ts @@ -268,6 +268,7 @@ function prepareBuiltins(oldBuiltins: Map) { } } newBuiltins.set('undefined', undefined) + newBuiltins.set('Object', Object) return newBuiltins } diff --git a/src/runner/sourceRunner.ts b/src/runner/sourceRunner.ts index 4a5310475..b7027d3e2 100644 --- a/src/runner/sourceRunner.ts +++ b/src/runner/sourceRunner.ts @@ -284,7 +284,7 @@ export async function sourceRunner( return runNative(program, context, theOptions) } - return runInterpreter(program!, context, theOptions) + return runInterpreter(program, context, theOptions) } export async function sourceFilesRunner( diff --git a/src/transpiler/__tests__/modules.ts b/src/transpiler/__tests__/modules.ts index 0bba8c24c..d40396974 100644 --- a/src/transpiler/__tests__/modules.ts +++ b/src/transpiler/__tests__/modules.ts @@ -11,12 +11,30 @@ import { transformImportDeclarations, transpile } from '../transpiler' jest.mock('../../modules/moduleLoaderAsync') jest.mock('../../modules/moduleLoader') +// One import declaration is transformed into two AST nodes +const IMPORT_DECLARATION_NODE_COUNT = 2 + +test('Transform import declarations to correct number of nodes', async () => { + const code = stripIndent` + import { foo } from "one_module"; + ` + const context = mockContext(Chapter.SOURCE_4) + const program = parse(code, context)! + const [, importNodes] = await transformImportDeclarations(program, new Set(), { + wrapSourceModules: true, + loadTabs: false, + checkImports: true + }) + expect(importNodes.length).toEqual(IMPORT_DECLARATION_NODE_COUNT) +}) + test('Transform import declarations into variable declarations', async () => { const code = stripIndent` import { foo } from "one_module"; import { bar } from "another_module"; foo(bar); ` + const identifiers = ['foo', 'bar'] as const const context = mockContext(Chapter.SOURCE_4) const program = parse(code, context)! const [, importNodes] = await transformImportDeclarations(program, new Set(), { @@ -25,11 +43,33 @@ test('Transform import declarations into variable declarations', async () => { checkImports: true }) - expect(importNodes[0].type).toBe('VariableDeclaration') - expect((importNodes[0].declarations[0].id as Identifier).name).toEqual('foo') + identifiers.forEach((ident, index) => { + const idx = IMPORT_DECLARATION_NODE_COUNT * index + expect(importNodes[idx].type).toBe('VariableDeclaration') + const node = importNodes[idx] as VariableDeclaration + expect((node.declarations[0].id as Identifier).name).toEqual(ident) + }) +}) + +test('Transform import declarations with correctly aliased names (expression statements)', async () => { + const code = stripIndent` + import { foo } from "one_module"; + import { bar as alias } from "another_module"; + foo(bar); + ` + const aliases = ['foo', 'alias'] as const + const context = mockContext(Chapter.SOURCE_4) + const program = parse(code, context)! + const [, importNodes] = await transformImportDeclarations(program, new Set(), { + wrapSourceModules: true, + loadTabs: false, + checkImports: true + }) - expect(importNodes[1].type).toBe('VariableDeclaration') - expect((importNodes[1].declarations[0].id as Identifier).name).toEqual('bar') + aliases.forEach((_, index) => { + const idx = IMPORT_DECLARATION_NODE_COUNT * index + 1 + expect(importNodes[idx].type).toBe('ExpressionStatement') + }) }) test('Transpiler accounts for user variable names when transforming import statements', async () => { @@ -54,7 +94,10 @@ test('Transpiler accounts for user variable names when transforming import state expect(importNodes[0].type).toBe('VariableDeclaration') expect( - ((importNodes[0].declarations[0].init as MemberExpression).object as Identifier).name + ( + ((importNodes[0] as VariableDeclaration).declarations[0].init as MemberExpression) + .object as Identifier + ).name ).toEqual('__MODULE__1') expect(varDecl0.type).toBe('VariableDeclaration') @@ -63,9 +106,12 @@ test('Transpiler accounts for user variable names when transforming import state expect(varDecl1.type).toBe('VariableDeclaration') expect(((varDecl1 as VariableDeclaration).declarations[0].init as Literal).value).toEqual('test1') - expect(importNodes[1].type).toBe('VariableDeclaration') + expect(importNodes[2].type).toBe('VariableDeclaration') expect( - ((importNodes[1].declarations[0].init as MemberExpression).object as Identifier).name + ( + ((importNodes[2] as VariableDeclaration).declarations[0].init as MemberExpression) + .object as Identifier + ).name ).toEqual('__MODULE__3') }) diff --git a/src/transpiler/transpiler.ts b/src/transpiler/transpiler.ts index e14b9acc7..0fb6535d7 100644 --- a/src/transpiler/transpiler.ts +++ b/src/transpiler/transpiler.ts @@ -63,7 +63,7 @@ export async function transformImportDeclarations( context?: Context, nativeId?: es.Identifier, useThis: boolean = false -): Promise<[string, es.VariableDeclaration[], es.Program['body']]> { +): Promise<[string, (es.VariableDeclaration | es.ExpressionStatement)[], es.Program['body']]> { const [importNodes, otherNodes] = partition(program.body, isImportDeclaration) if (importNodes.length === 0) return ['', [], otherNodes] @@ -109,21 +109,41 @@ export async function transformImportDeclarations( } const declNodes = nodes.flatMap(({ specifiers }) => - specifiers.map(spec => { + specifiers.flatMap(spec => { assert(spec.type === 'ImportSpecifier', `Expected ImportSpecifier, got ${spec.type}`) if (checkImports && !(spec.imported.name in docs!)) { throw new UndefinedImportError(spec.imported.name, moduleName, spec) } - // Convert each import specifier to its corresponding local variable declaration - return create.constantDeclaration( - spec.local.name, - create.memberExpression( - create.identifier(`${useThis ? 'this.' : ''}${namespaced}`), - spec.imported.name + return [ + // Convert each import specifier to its corresponding local variable declaration + create.constantDeclaration( + spec.local.name, + create.memberExpression( + create.identifier(`${useThis ? 'this.' : ''}${namespaced}`), + spec.imported.name + ) + ), + // Update the specifier's name property with the new name. This is so that calling + // `function.name` will return the aliased name. Equivalent to: + // Object.defineProperty(spec.imported.name, 'name', { value: spec.local.name }); + create.expressionStatement( + create.callExpression( + create.memberExpression(create.identifier('Object'), 'defineProperty'), + [ + create.memberExpression( + create.identifier(`${useThis ? 'this.' : ''}${namespaced}`), + spec.imported.name + ), + create.literal('name'), + create.objectExpression([ + create.property('value', create.literal(spec.local.name)) + ]) + ] + ) ) - ) + ] }) ) @@ -131,7 +151,7 @@ export async function transformImportDeclarations( string, { text: string - nodes: es.VariableDeclaration[] + nodes: (es.VariableDeclaration | es.ExpressionStatement)[] namespaced: string } ]