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

Add proof-of-concept for handling module types in Source Typed #1467

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions src/errors/typeErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,3 +727,26 @@ export class DuplicateTypeAliasError implements SourceError {
return this.explain()
}
}

export class NameNotFoundInModuleError implements SourceError {
public type = ErrorType.TYPE
public severity = ErrorSeverity.ERROR

constructor(
public node: tsEs.ImportDeclaration,
public moduleName: string,
public name: string
) {}

get location() {
return this.node.loc ?? UNKNOWN_LOCATION
}

public explain() {
return `Module '${this.moduleName}' has no exported member '${this.name}'.`
}

public elaborate() {
return this.explain()
}
}
42 changes: 40 additions & 2 deletions src/typeChecker/__tests__/source1Typed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,17 +1016,55 @@ describe('import statements', () => {
)
})

it('defaults to any for all imports', () => {
const code = `import { show, heart } from 'rune';
it('throws error if module does not exist', () => {
const code = `import { show, heart } from 'doesnotexist';
show(heart);
heart(show);
const x1: string = heart;
const x2: number = show;
`

parse(code, context)
expect(parseError(context.errors)).toMatchInlineSnapshot(
`"Line 1: Module \\"doesnotexist\\" not found."`
)
})

it('defaults to any for modules without types', () => {
const code = `import { sine_sound } from 'sound';
sine_sound(440, 5);
sine_sound('440', '5');
`

parse(code, context)
expect(parseError(context.errors)).toMatchInlineSnapshot(`""`)
})

it('throws error if name does not exist in typed module', () => {
const code = `import { show, doesnotexist } from 'rune';
show(doesnotexist);
`

parse(code, context)
expect(parseError(context.errors)).toMatchInlineSnapshot(
`"Line 1: Module 'rune' has no exported member 'doesnotexist'."`
)
})

it('handles types correctly for typed modules', () => {
const code = `import { show, heart } from 'rune';
show(heart);
heart(show);
const x1: string = heart; // unfortunately passes typechecking since placeholder for Rune type is currently a string
const x2: number = show;
`

parse(code, context)
expect(parseError(context.errors)).toMatchInlineSnapshot(`
"Line 3: Type 'Rune' is not callable.
Line 5: Type '(Rune) => Rune' is not assignable to type 'number'."
`)
})
})

describe('scoping', () => {
Expand Down
129 changes: 129 additions & 0 deletions src/typeChecker/runeTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
export const runeTypeDeclarations = {
prelude: `type Rune = 'Rune';
type AnimatedRune = 'AnimatedRune';`,
blank: `const blank: Rune = 'Rune';`,
circle: `const circle: Rune = 'Rune';`,
corner: `const corner: Rune = 'Rune';`,
heart: `const heart: Rune = 'Rune';`,
nova: `const nova: Rune = 'Rune';`,
pentagram: `const pentagram: Rune = 'Rune';`,
rcross: `const rcross: Rune = 'Rune';`,
ribbon: `const ribbon: Rune = 'Rune';`,
sail: `const sail: Rune = 'Rune';`,
square: `const square: Rune = 'Rune';`,
triangle: `const triangle: Rune = 'Rune';`,
black: `function black(rune: Rune): Rune {
return 'Rune';
}`,
blue: `function blue(rune: Rune): Rune {
return 'Rune';
}`,
brown: `function brown(rune: Rune): Rune {
return 'Rune';
}`,
color: `function color(rune: Rune, r: number, g: number, b: number): Rune {
return 'Rune';
}`,
green: `function green(rune: Rune): Rune {
return 'Rune';
}`,
indigo: `function indigo(rune: Rune): Rune {
return 'Rune';
}`,
orange: `function orange(rune: Rune): Rune {
return 'Rune';
}`,
pink: `function pink(rune: Rune): Rune {
return 'Rune';
}`,
purple: `function purple(rune: Rune): Rune {
return 'Rune';
}`,
random_color: `function random_color(rune: Rune): Rune {
return 'Rune';
}`,
red: `function red(rune: Rune): Rune {
return 'Rune';
}`,
white: `function white(rune: Rune): Rune {
return 'Rune';
}`,
yellow: `function yellow(rune: Rune): Rune {
return 'Rune';
}`,
anaglyph: `function anaglyph(rune: Rune): Rune {
return 'Rune';
}`,
animate_anaglyph: `animate_anaglyph(duration: number, fps: number, func: RuneAnimation): AnimatedRune {
return 'AnimatedRune';
}`,
animate_rune: `function animate_rune(duration: number, fps: number, func: RuneAnimation): AnimatedRune {
return 'AnimatedRune';
}`,
beside: `function beside(rune1: Rune, rune2: Rune): Rune {
return 'Rune';
}`,
beside_frac: `function beside_frac(frac: number, rune1: Rune, rune2: Rune): Rune {
return 'Rune';
}`,
flip_horiz: `function flip_horiz(rune: Rune): Rune {
return 'Rune';
}`,
flip_vert: `function flip_vert(rune: Rune): Rune {
return 'Rune';
}`,
from_url: `function from_url(imageUrl: string): Rune {
return 'Rune';
}`,
hollusion: `function hollusion(rune: Rune): Rune {
return 'Rune';
}`,
hollusion_magnitude: `function hollusion_magnitude(rune: Rune, magnitude: number): Rune {
return 'Rune';
}`,
make_cross: `function make_cross(rune: Rune): Rune {
return 'Rune';
}`,
overlay: `function overlay(rune1: Rune, rune2: Rune): Rune {
return 'Rune';
}`,
overlay_frac: `function overlay_frac(frac: number, rune1: Rune, rune2: Rune): Rune {
return 'Rune';
}`,
quarter_turn_left: `function quarter_turn_left(rune: Rune): Rune {
return 'Rune';
}`,
quarter_turn_right: `function quarter_turn_right(rune: Rune): Rune {
return 'Rune';
}`,
repeat_pattern: `function repeat_pattern(n: number, pattern: ((a: Rune) => Rune), initial: Rune): Rune {
return 'Rune';
}`,
rotate: `function rotate(rad: number, rune: Rune): Rune {
return 'Rune';
}`,
scale: `function scale(ratio: number, rune: Rune): Rune {
return 'Rune';
}`,
scale_independent: `function scale_independent(ratio_x: number, ratio_y: number, rune: Rune): Rune {
return 'Rune';
}`,
show: `function show(rune: Rune): Rune {
return 'Rune';
}`,
stack: `function stack(rune1: Rune, rune2: Rune): Rune {
return 'Rune';
}`,
stack_frac: `function stack_frac(frac: number, rune1: Rune, rune2: Rune): Rune {
return 'Rune';
}`,
stackn: `function stackn(n: number, rune: Rune): Rune {
return 'Rune';
}`,
translate: `function translate(x: number, y: number, rune: Rune): Rune {
return 'Rune';
}`,
turn_upside_down: `function turn_upside_down(rune: Rune): Rune {
return 'Rune';
}`
}
73 changes: 67 additions & 6 deletions src/typeChecker/typeErrorChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
InvalidIndexTypeError,
InvalidNumberOfArgumentsTypeError,
InvalidNumberOfTypeArgumentsForGenericTypeError,
NameNotFoundInModuleError,
TypeAliasNameNotAllowedError,
TypecastError,
TypeMismatchError,
Expand Down Expand Up @@ -39,6 +40,7 @@ import {
} from '../types'
import { TypecheckError } from './internalTypeErrors'
import { parseTreeTypesPrelude } from './parseTreeTypes.prelude'
import { runeTypeDeclarations } from './runeTypes'
import * as tsEs from './tsESTree'
import {
formatTypeString,
Expand Down Expand Up @@ -385,7 +387,8 @@ function typeCheckAndReturnType(node: tsEs.Node): Type {
return tStream(elementType)
}
}
const calleeType = typeCheckAndReturnType(callee)
// Copy of callee type is made so that the type saved in the type environment does not change
const calleeType = cloneDeep(typeCheckAndReturnType(callee))
if (calleeType.kind !== 'function') {
if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
context.errors.push(new TypeNotCallableError(node, formatTypeString(calleeType)))
Expand Down Expand Up @@ -547,22 +550,78 @@ function handleImportDeclarations(node: tsEs.Program) {
if (importStmts.length === 0) {
return
}
const modules = memoizedGetModuleManifest()
const moduleList = Object.keys(modules)
const moduleList = Object.keys(memoizedGetModuleManifest())
const importedModuleTypesTextMap: Record<string, string> = {}

importStmts.forEach(stmt => {
// Source only uses strings for import source value
const moduleName = stmt.source.value as string

// Module not found
if (!moduleList.includes(moduleName)) {
context.errors.push(new ModuleNotFoundError(moduleName, stmt))
// Set all imported names to be of type any to prevent further typecheck errors
stmt.specifiers.map(spec => {
if (spec.type !== 'ImportSpecifier') {
throw new TypecheckError(stmt, 'Unknown specifier type')
}
setType(spec.local.name, tAny, env)
})
return
}

// TODO: Add logic for fetching module type declarations map from modules repo
// after the modules have been properly typed on modules repo.
// runeTypes.ts is currently a temporary file added to js-slang as a proof of concept;
// the file should be deleted once the module types fetching system has been properly implemented
const moduleTypesTextMap = moduleName === 'rune' ? runeTypeDeclarations : undefined

// Module has no types
if (!moduleTypesTextMap) {
// Set all imported names to be of type any
// TODO: Consider switching to 'Module not supported' error after more modules have been typed
stmt.specifiers.map(spec => {
if (spec.type !== 'ImportSpecifier') {
throw new TypecheckError(stmt, 'Unknown specifier type')
}
setType(spec.local.name, tAny, env)
})
return
}
stmt.specifiers.map(spec => {

// Add prelude for module, which contains types that are shared by
// multiple variables and functions in the module
if (!importedModuleTypesTextMap[moduleName]) {
importedModuleTypesTextMap[moduleName] = moduleTypesTextMap.prelude
}

stmt.specifiers.forEach(spec => {
if (spec.type !== 'ImportSpecifier') {
throw new TypecheckError(stmt, 'Unknown specifier type')
}

setType(spec.local.name, tAny, env)
const importedName = spec.local.name
const importedType = moduleTypesTextMap[importedName]
if (!importedType) {
context.errors.push(new NameNotFoundInModuleError(stmt, moduleName, importedName))
// Set imported name to be of type any to prevent further typecheck errors
setType(importedName, tAny, env)
return
}

importedModuleTypesTextMap[moduleName] =
importedModuleTypesTextMap[moduleName] + '\n' + importedType
})
})

// Add module types to type environment
Object.values(importedModuleTypesTextMap).forEach(typesText => {
const parsedModuleTypes = babelParse(typesText, {
sourceType: 'module',
plugins: ['typescript', 'estree']
}).program as unknown as tsEs.Program
typeCheckAndReturnType(parsedModuleTypes)
})
}

/**
Expand Down Expand Up @@ -801,7 +860,9 @@ function getTypeVariableMappings(
expectedType: Type
): [string, Type][] {
// If type variable mapping is found, terminate early
if (expectedType.kind === 'variable') {
// note that the expected type is only a type variable
// if there is no type alias with the same name already saved in the type env
if (expectedType.kind === 'variable' && !lookupTypeAlias(expectedType.name, env)) {
return [[expectedType.name, actualType]]
}
// If actual type is a type reference, expand type first
Expand Down
Loading