js-slang
Version:
Javascript-based implementations of Source, written in Typescript
934 lines (933 loc) • 63.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeTSNodes = exports.checkForTypeErrors = void 0;
const parser_1 = require("@babel/parser");
const lodash_1 = require("lodash");
const typeErrors_1 = require("../errors/typeErrors");
const types_1 = require("../types");
const internalTypeErrors_1 = require("./internalTypeErrors");
const parseTreeTypes_prelude_1 = require("./parseTreeTypes.prelude");
const utils_1 = require("./utils");
// Context and type environment are saved as global variables so that they are not passed between functions excessively
let context = {};
let env = [];
/**
* Entry function for type error checker.
* Checks program for type errors, and returns the program with all TS-related nodes removed.
*/
function checkForTypeErrors(program, inputContext) {
// Set context as global variable
context = inputContext;
// Deep copy type environment to avoid modifying type environment in the context,
// which might affect the type inference checker
env = (0, lodash_1.cloneDeep)(context.typeEnvironment);
// Override predeclared function types
for (const [name, type] of (0, utils_1.getTypeOverrides)(context.chapter)) {
(0, utils_1.setType)(name, type, env);
}
if (context.chapter >= 4) {
// Add parse tree types to type environment
const source4Types = (0, parser_1.parse)(parseTreeTypes_prelude_1.parseTreeTypesPrelude, {
sourceType: 'module',
plugins: ['typescript', 'estree']
}).program;
typeCheckAndReturnType(source4Types);
}
try {
typeCheckAndReturnType(program);
}
catch (error) {
// Catch-all for thrown errors
// (either errors that cause early termination or errors that should not be reached logically)
console.error(error);
context.errors.push(error instanceof internalTypeErrors_1.TypecheckError
? error
: new internalTypeErrors_1.TypecheckError(program, 'Uncaught error during typechecking, report this to the administrators!\n' +
error.message));
}
// Reset global variables
context = {};
env = [];
return removeTSNodes(program);
}
exports.checkForTypeErrors = checkForTypeErrors;
/**
* Recurses through the given node to check for any type errors,
* then returns the node's inferred/declared type.
* Any errors found are added to the context.
*/
function typeCheckAndReturnType(node) {
switch (node.type) {
case 'Literal': {
// Infers type
if (node.value === undefined) {
return utils_1.tUndef;
}
if (node.value === null) {
// For Source 1, skip typecheck as null literals will be handled by the noNull rule,
// which is run after typechecking
return context.chapter === types_1.Chapter.SOURCE_1 ? utils_1.tAny : utils_1.tNull;
}
if (typeof node.value !== 'string' &&
typeof node.value !== 'number' &&
typeof node.value !== 'boolean') {
// Skip typecheck as unspecified literals will be handled by the noUnspecifiedLiteral rule,
// which is run after typechecking
return utils_1.tAny;
}
// Casting is safe here as above check already narrows type to string, number or boolean
return (0, utils_1.tPrimitive)(typeof node.value, node.value);
}
case 'TemplateLiteral': {
// Quasis array should only have one element as
// string interpolation is not allowed in Source
return (0, utils_1.tPrimitive)('string', node.quasis[0].value.raw);
}
case 'Identifier': {
const varName = node.name;
const varType = lookupTypeAndRemoveForAllAndPredicateTypes(varName);
if (varType) {
return varType;
}
else {
context.errors.push(new typeErrors_1.UndefinedVariableTypeError(node, varName));
return utils_1.tAny;
}
}
case 'RestElement':
case 'SpreadElement':
// TODO: Add support for rest and spread element
return utils_1.tAny;
case 'Program':
case 'BlockStatement': {
let returnType = utils_1.tVoid;
(0, utils_1.pushEnv)(env);
if (node.type === 'Program') {
// Import statements should only exist in program body
handleImportDeclarations(node);
}
// Add all declarations in the current scope to the environment first
addTypeDeclarationsToEnvironment(node);
// Check all statements in program/block body
for (const stmt of node.body) {
if (stmt.type === 'IfStatement' || stmt.type === 'ReturnStatement') {
returnType = typeCheckAndReturnType(stmt);
if (stmt.type === 'ReturnStatement') {
// If multiple return statements are present, only take the first type
break;
}
}
else {
typeCheckAndReturnType(stmt);
}
}
if (node.type === 'BlockStatement') {
// Types are saved for programs, but not for blocks
env.pop();
}
return returnType;
}
case 'ExpressionStatement': {
// Check expression
return typeCheckAndReturnType(node.expression);
}
case 'ConditionalExpression':
case 'IfStatement': {
// Typecheck predicate against boolean
const predicateType = typeCheckAndReturnType(node.test);
checkForTypeMismatch(node, predicateType, utils_1.tBool);
// Return type is union of consequent and alternate type
const consType = typeCheckAndReturnType(node.consequent);
const altType = node.alternate ? typeCheckAndReturnType(node.alternate) : utils_1.tUndef;
return mergeTypes(node, consType, altType);
}
case 'UnaryExpression': {
const argType = typeCheckAndReturnType(node.argument);
const operator = node.operator;
switch (operator) {
case '-':
// Typecheck against number
checkForTypeMismatch(node, argType, utils_1.tNumber);
return utils_1.tNumber;
case '!':
// Typecheck against boolean
checkForTypeMismatch(node, argType, utils_1.tBool);
return utils_1.tBool;
case 'typeof':
// No checking needed, typeof operation can be used on any type
return utils_1.tString;
default:
throw new internalTypeErrors_1.TypecheckError(node, 'Unknown operator');
}
}
case 'BinaryExpression': {
return typeCheckAndReturnBinaryExpressionType(node);
}
case 'LogicalExpression': {
// Typecheck left type against boolean
const leftType = typeCheckAndReturnType(node.left);
checkForTypeMismatch(node, leftType, utils_1.tBool);
// Return type is union of boolean and right type
const rightType = typeCheckAndReturnType(node.right);
return mergeTypes(node, utils_1.tBool, rightType);
}
case 'ArrowFunctionExpression': {
return typeCheckAndReturnArrowFunctionType(node);
}
case 'FunctionDeclaration':
if (node.id === null) {
// Block should not be reached since node.id is only null when function declaration
// is part of `export default function`, which is not used in Source
throw new internalTypeErrors_1.TypecheckError(node, 'Function declaration should always have an identifier');
}
// Only identifiers/rest elements are used as function params in Source
const params = node.params.filter((param) => param.type === 'Identifier' || param.type === 'RestElement');
if (params.length !== node.params.length) {
throw new internalTypeErrors_1.TypecheckError(node, 'Unknown function parameter type');
}
const fnName = node.id.name;
const expectedReturnType = getTypeAnnotationType(node.returnType);
// If the function has variable number of arguments, set function type as any
// TODO: Add support for variable number of function arguments
const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
if (hasVarArgs) {
(0, utils_1.setType)(fnName, utils_1.tAny, env);
return utils_1.tUndef;
}
const types = getParamTypes(params);
// Return type will always be last item in types array
types.push(expectedReturnType);
const fnType = (0, utils_1.tFunc)(...types);
// Typecheck function body, creating new environment to store arg types, return type and function type
(0, utils_1.pushEnv)(env);
params.forEach((param) => {
(0, utils_1.setType)(param.name, getTypeAnnotationType(param.typeAnnotation), env);
});
// Set unique identifier so that typechecking can be carried out for return statements
(0, utils_1.setType)(utils_1.RETURN_TYPE_IDENTIFIER, expectedReturnType, env);
(0, utils_1.setType)(fnName, fnType, env);
const actualReturnType = typeCheckAndReturnType(node.body);
env.pop();
if ((0, lodash_1.isEqual)(actualReturnType, utils_1.tVoid) &&
!(0, lodash_1.isEqual)(expectedReturnType, utils_1.tAny) &&
!(0, lodash_1.isEqual)(expectedReturnType, utils_1.tVoid)) {
// Type error where function does not return anything when it should
context.errors.push(new typeErrors_1.FunctionShouldHaveReturnValueError(node));
}
else {
checkForTypeMismatch(node, actualReturnType, expectedReturnType);
}
// Save function type in type env
(0, utils_1.setType)(fnName, fnType, env);
return utils_1.tUndef;
case 'VariableDeclaration': {
if (node.kind === 'var') {
throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration using "var" is not allowed');
}
if (node.declarations.length !== 1) {
throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration should have one and only one declaration');
}
if (node.declarations[0].id.type !== 'Identifier') {
throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration ID should be an identifier');
}
const id = node.declarations[0].id;
if (!node.declarations[0].init) {
throw new internalTypeErrors_1.TypecheckError(node, 'Variable declaration must have value');
}
const init = node.declarations[0].init;
// Look up declared type if current environment contains name
const expectedType = env[env.length - 1].typeMap.has(id.name)
? lookupTypeAndRemoveForAllAndPredicateTypes(id.name) ??
getTypeAnnotationType(id.typeAnnotation)
: getTypeAnnotationType(id.typeAnnotation);
const initType = typeCheckAndReturnType(init);
checkForTypeMismatch(node, initType, expectedType);
// Save variable type and decl kind in type env
(0, utils_1.setType)(id.name, expectedType, env);
(0, utils_1.setDeclKind)(id.name, node.kind, env);
return utils_1.tUndef;
}
case 'CallExpression': {
const callee = node.callee;
const args = node.arguments;
if (context.chapter >= 2 && callee.type === 'Identifier') {
// Special functions for Source 2+: list, head, tail, stream
// The typical way of getting the return type of call expressions is insufficient to type lists,
// as we need to save the pair representation of the list as well (lists are pairs).
// head and tail should preserve the pair representation of lists whenever possible.
// Hence, these 3 functions are handled separately.
// Streams are treated similarly to lists, except only for Source 3+ and we do not need to store the pair representation.
const fnName = callee.name;
if (fnName === 'list') {
if (args.length === 0) {
return utils_1.tNull;
}
// Element type is union of all types of arguments in list
let elementType = typeCheckAndReturnType(args[0]);
for (let i = 1; i < args.length; i++) {
elementType = mergeTypes(node, elementType, typeCheckAndReturnType(args[i]));
}
// Type the list as a pair, for use when checking for type mismatches against pairs
let pairType = (0, utils_1.tPair)(typeCheckAndReturnType(args[args.length - 1]), utils_1.tNull);
for (let i = args.length - 2; i >= 0; i--) {
pairType = (0, utils_1.tPair)(typeCheckAndReturnType(args[i]), pairType);
}
return (0, utils_1.tList)(elementType, pairType);
}
if (fnName === 'head' || fnName === 'tail') {
if (args.length !== 1) {
context.errors.push(new typeErrors_1.InvalidNumberOfArgumentsTypeError(node, 1, args.length));
return utils_1.tAny;
}
const actualType = typeCheckAndReturnType(args[0]);
// Argument should be either a pair or a list
const expectedType = (0, utils_1.tUnion)((0, utils_1.tPair)(utils_1.tAny, utils_1.tAny), (0, utils_1.tList)(utils_1.tAny));
const numErrors = context.errors.length;
checkForTypeMismatch(node, actualType, expectedType);
if (context.errors.length > numErrors) {
// If errors were found, return "any" type
return utils_1.tAny;
}
return fnName === 'head' ? getHeadType(node, actualType) : getTailType(node, actualType);
}
if (fnName === 'stream' && context.chapter >= 3) {
if (args.length === 0) {
return utils_1.tNull;
}
// Element type is union of all types of arguments in stream
let elementType = typeCheckAndReturnType(args[0]);
for (let i = 1; i < args.length; i++) {
elementType = mergeTypes(node, elementType, typeCheckAndReturnType(args[i]));
}
return (0, utils_1.tStream)(elementType);
}
}
const calleeType = typeCheckAndReturnType(callee);
if (calleeType.kind !== 'function') {
if (calleeType.kind !== 'primitive' || calleeType.name !== 'any') {
context.errors.push(new typeErrors_1.TypeNotCallableError(node, (0, utils_1.formatTypeString)(calleeType)));
}
return utils_1.tAny;
}
const expectedTypes = calleeType.parameterTypes;
let returnType = calleeType.returnType;
// If any of the arguments is a spread element, skip type checking of arguments
// TODO: Add support for type checking of call expressions with spread elements
const hasVarArgs = args.reduce((prev, curr) => prev || curr.type === 'SpreadElement', false);
if (hasVarArgs) {
return returnType;
}
// Check argument types before returning declared return type
if (args.length !== expectedTypes.length) {
context.errors.push(new typeErrors_1.InvalidNumberOfArgumentsTypeError(node, expectedTypes.length, args.length));
return returnType;
}
for (let i = 0; i < expectedTypes.length; i++) {
const node = args[i];
const actualType = typeCheckAndReturnType(node);
// Get all valid type variable mappings for current argument
const mappings = getTypeVariableMappings(node, actualType, expectedTypes[i]);
// Apply type variable mappings to subsequent argument types and return type
for (const mapping of mappings) {
const typeVar = (0, utils_1.tVar)(mapping[0]);
const typeToSub = mapping[1];
for (let j = i; j < expectedTypes.length; j++) {
expectedTypes[j] = substituteVariableTypes(expectedTypes[j], typeVar, typeToSub);
}
returnType = substituteVariableTypes(returnType, typeVar, typeToSub);
}
// Typecheck current argument
checkForTypeMismatch(node, actualType, expectedTypes[i]);
}
return returnType;
}
case 'AssignmentExpression':
const expectedType = typeCheckAndReturnType(node.left);
const actualType = typeCheckAndReturnType(node.right);
if (node.left.type === 'Identifier' && (0, utils_1.lookupDeclKind)(node.left.name, env) === 'const') {
context.errors.push(new typeErrors_1.ConstNotAssignableTypeError(node, node.left.name));
}
checkForTypeMismatch(node, actualType, expectedType);
return actualType;
case 'ArrayExpression':
// Casting is safe here as Source disallows use of spread elements and holes in arrays
const elements = node.elements.filter((elem) => elem !== null && elem.type !== 'SpreadElement');
if (elements.length !== node.elements.length) {
throw new internalTypeErrors_1.TypecheckError(node, 'Disallowed array element type');
}
if (elements.length === 0) {
return (0, utils_1.tArray)(utils_1.tAny);
}
const elementTypes = elements.map(elem => typeCheckAndReturnType(elem));
return (0, utils_1.tArray)(mergeTypes(node, ...elementTypes));
case 'MemberExpression':
const indexType = typeCheckAndReturnType(node.property);
const objectType = typeCheckAndReturnType(node.object);
// Typecheck index against number
if (hasTypeMismatchErrors(node, indexType, utils_1.tNumber)) {
context.errors.push(new typeErrors_1.InvalidIndexTypeError(node, (0, utils_1.formatTypeString)(indexType, true)));
}
// Expression being accessed must be array
if (objectType.kind !== 'array') {
context.errors.push(new typeErrors_1.InvalidArrayAccessTypeError(node, (0, utils_1.formatTypeString)(objectType)));
return utils_1.tAny;
}
return objectType.elementType;
case 'ReturnStatement': {
if (!node.argument) {
// Skip typecheck as unspecified literals will be handled by the noImplicitReturnUndefined rule,
// which is run after typechecking
return utils_1.tUndef;
}
else {
// Check type only if return type is specified
const expectedType = lookupTypeAndRemoveForAllAndPredicateTypes(utils_1.RETURN_TYPE_IDENTIFIER);
if (expectedType) {
const argumentType = typeCheckAndReturnType(node.argument);
checkForTypeMismatch(node, argumentType, expectedType);
return expectedType;
}
else {
return typeCheckAndReturnType(node.argument);
}
}
}
case 'WhileStatement': {
// Typecheck predicate against boolean
const testType = typeCheckAndReturnType(node.test);
checkForTypeMismatch(node, testType, utils_1.tBool);
return typeCheckAndReturnType(node.body);
}
case 'ForStatement': {
// Add new environment so that new variable declared in init node can be isolated to within for statement only
(0, utils_1.pushEnv)(env);
if (node.init) {
typeCheckAndReturnType(node.init);
}
if (node.test) {
// Typecheck predicate against boolean
const testType = typeCheckAndReturnType(node.test);
checkForTypeMismatch(node, testType, utils_1.tBool);
}
if (node.update) {
typeCheckAndReturnType(node.update);
}
const bodyType = typeCheckAndReturnType(node.body);
env.pop();
return bodyType;
}
case 'ImportDeclaration':
// No typechecking needed, import declarations have already been handled separately
return utils_1.tUndef;
case 'TSTypeAliasDeclaration':
// No typechecking needed, type has already been added to environment
return utils_1.tUndef;
case 'TSAsExpression':
const originalType = typeCheckAndReturnType(node.expression);
const typeToCastTo = getTypeAnnotationType(node);
const formatAsLiteral = typeContainsLiteralType(originalType) || typeContainsLiteralType(typeToCastTo);
// Type to cast to must have some overlap with original type
if (hasTypeMismatchErrors(node, typeToCastTo, originalType)) {
context.errors.push(new typeErrors_1.TypecastError(node, (0, utils_1.formatTypeString)(originalType, formatAsLiteral), (0, utils_1.formatTypeString)(typeToCastTo, formatAsLiteral)));
}
return typeToCastTo;
case 'TSInterfaceDeclaration':
throw new internalTypeErrors_1.TypecheckError(node, 'Interface declarations are not allowed');
case 'ExportNamedDeclaration':
return typeCheckAndReturnType(node.declaration);
default:
throw new internalTypeErrors_1.TypecheckError(node, 'Unknown node type');
}
}
/**
* Adds types for imported functions to the type environment.
* All imports have their types set to the "any" primitive type.
*/
function handleImportDeclarations(node) {
const importStmts = node.body.filter((stmt) => stmt.type === 'ImportDeclaration');
if (importStmts.length === 0) {
return;
}
importStmts.forEach(stmt => {
// Source only uses strings for import source value
stmt.specifiers.map(spec => {
(0, utils_1.setType)(spec.local.name, utils_1.tAny, env);
});
});
}
/**
* Adds all types for variable/function/type declarations to the current environment.
* This is so that the types can be referenced before the declarations are initialized.
* Type checking is not carried out as this function is only responsible for hoisting declarations.
*/
function addTypeDeclarationsToEnvironment(node) {
node.body.forEach(bodyNode => {
switch (bodyNode.type) {
case 'FunctionDeclaration':
if (bodyNode.id === null) {
throw new Error('Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.');
}
// Only identifiers/rest elements are used as function params in Source
const params = bodyNode.params.filter((param) => param.type === 'Identifier' || param.type === 'RestElement');
if (params.length !== bodyNode.params.length) {
throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Unknown function parameter type');
}
const fnName = bodyNode.id.name;
const returnType = getTypeAnnotationType(bodyNode.returnType);
// If the function has variable number of arguments, set function type as any
// TODO: Add support for variable number of function arguments
const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
if (hasVarArgs) {
(0, utils_1.setType)(fnName, utils_1.tAny, env);
break;
}
const types = getParamTypes(params);
// Return type will always be last item in types array
types.push(returnType);
const fnType = (0, utils_1.tFunc)(...types);
// Save function type in type env
(0, utils_1.setType)(fnName, fnType, env);
break;
case 'VariableDeclaration':
if (bodyNode.kind === 'var') {
throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Variable declaration using "var" is not allowed');
}
if (bodyNode.declarations.length !== 1) {
throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Variable declaration should have one and only one declaration');
}
if (bodyNode.declarations[0].id.type !== 'Identifier') {
throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Variable declaration ID should be an identifier');
}
const id = bodyNode.declarations[0].id;
const expectedType = getTypeAnnotationType(id.typeAnnotation);
// Save variable type and decl kind in type env
(0, utils_1.setType)(id.name, expectedType, env);
(0, utils_1.setDeclKind)(id.name, bodyNode.kind, env);
break;
case 'TSTypeAliasDeclaration':
if (node.type === 'BlockStatement') {
throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Type alias declarations may only appear at the top level');
}
const alias = bodyNode.id.name;
if (Object.values(utils_1.typeAnnotationKeywordToBasicTypeMap).includes(alias)) {
context.errors.push(new typeErrors_1.TypeAliasNameNotAllowedError(bodyNode, alias));
break;
}
if ((0, utils_1.lookupTypeAlias)(alias, env) !== undefined) {
// Only happens when attempting to declare type aliases that share names with predeclared types (e.g. Pair, List)
// Declaration of two type aliases with the same name will be caught as syntax error by parser
context.errors.push(new typeErrors_1.DuplicateTypeAliasError(bodyNode, alias));
break;
}
let type = utils_1.tAny;
if (bodyNode.typeParameters && bodyNode.typeParameters.params.length > 0) {
const typeParams = [];
// Check validity of type parameters
(0, utils_1.pushEnv)(env);
bodyNode.typeParameters.params.forEach(param => {
if (param.type !== 'TSTypeParameter') {
throw new internalTypeErrors_1.TypecheckError(bodyNode, 'Invalid type parameter type');
}
const name = param.name;
if (Object.values(utils_1.typeAnnotationKeywordToBasicTypeMap).includes(name)) {
context.errors.push(new typeErrors_1.TypeParameterNameNotAllowedError(param, name));
return;
}
typeParams.push((0, utils_1.tVar)(name));
});
type = (0, utils_1.tForAll)(getTypeAnnotationType(bodyNode), typeParams);
env.pop();
}
else {
type = getTypeAnnotationType(bodyNode);
}
(0, utils_1.setTypeAlias)(alias, type, env);
break;
default:
break;
}
});
}
/**
* Typechecks the body of a binary expression, adding any type errors to context if necessary.
* Then, returns the type of the binary expression, inferred based on the operator.
*/
function typeCheckAndReturnBinaryExpressionType(node) {
const leftType = typeCheckAndReturnType(node.left);
const rightType = typeCheckAndReturnType(node.right);
const leftTypeString = (0, utils_1.formatTypeString)(leftType);
const rightTypeString = (0, utils_1.formatTypeString)(rightType);
const operator = node.operator;
switch (operator) {
case '-':
case '*':
case '/':
case '%':
// Typecheck both sides against number
checkForTypeMismatch(node, leftType, utils_1.tNumber);
checkForTypeMismatch(node, rightType, utils_1.tNumber);
// Return type number
return utils_1.tNumber;
case '+':
// Typecheck both sides against number or string
// However, the case where one side is string and other side is number is not allowed
if (leftTypeString === 'number' || leftTypeString === 'string') {
checkForTypeMismatch(node, rightType, leftType);
// If left type is number or string, return left type
return leftType;
}
if (rightTypeString === 'number' || rightTypeString === 'string') {
checkForTypeMismatch(node, leftType, rightType);
// If left type is not number or string but right type is number or string, return right type
return rightType;
}
checkForTypeMismatch(node, leftType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString));
checkForTypeMismatch(node, rightType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString));
// Return type is number | string if both left and right are neither number nor string
return (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString);
case '<':
case '<=':
case '>':
case '>=':
case '!==':
case '===':
// In Source 3 and above, skip type checking as equality can be applied between two items of any type
if (context.chapter > 2 && (operator === '===' || operator === '!==')) {
return utils_1.tBool;
}
// Typecheck both sides against number or string
// However, case where one side is string and other side is number is not allowed
if (leftTypeString === 'number' || leftTypeString === 'string') {
checkForTypeMismatch(node, rightType, leftType);
return utils_1.tBool;
}
if (rightTypeString === 'number' || rightTypeString === 'string') {
checkForTypeMismatch(node, leftType, rightType);
return utils_1.tBool;
}
checkForTypeMismatch(node, leftType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString));
checkForTypeMismatch(node, rightType, (0, utils_1.tUnion)(utils_1.tNumber, utils_1.tString));
// Return type boolean
return utils_1.tBool;
default:
throw new internalTypeErrors_1.TypecheckError(node, 'Unknown operator');
}
}
/**
* Typechecks the body of an arrow function, adding any type errors to context if necessary.
* Then, returns the inferred/declared type of the function.
*/
function typeCheckAndReturnArrowFunctionType(node) {
// Only identifiers/rest elements are used as function params in Source
const params = node.params.filter((param) => param.type === 'Identifier' || param.type === 'RestElement');
if (params.length !== node.params.length) {
throw new internalTypeErrors_1.TypecheckError(node, 'Unknown function parameter type');
}
const expectedReturnType = getTypeAnnotationType(node.returnType);
// If the function has variable number of arguments, set function type as any
// TODO: Add support for variable number of function arguments
const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
if (hasVarArgs) {
return utils_1.tAny;
}
// Typecheck function body, creating new environment to store arg types and return type
(0, utils_1.pushEnv)(env);
params.forEach((param) => {
(0, utils_1.setType)(param.name, getTypeAnnotationType(param.typeAnnotation), env);
});
// Set unique identifier so that typechecking can be carried out for return statements
(0, utils_1.setType)(utils_1.RETURN_TYPE_IDENTIFIER, expectedReturnType, env);
const actualReturnType = typeCheckAndReturnType(node.body);
env.pop();
if ((0, lodash_1.isEqual)(actualReturnType, utils_1.tVoid) &&
!(0, lodash_1.isEqual)(expectedReturnType, utils_1.tAny) &&
!(0, lodash_1.isEqual)(expectedReturnType, utils_1.tVoid)) {
// Type error where function does not return anything when it should
context.errors.push(new typeErrors_1.FunctionShouldHaveReturnValueError(node));
}
else {
checkForTypeMismatch(node, actualReturnType, expectedReturnType);
}
const types = getParamTypes(params);
// Return type will always be last item in types array
types.push(node.returnType ? expectedReturnType : actualReturnType);
return (0, utils_1.tFunc)(...types);
}
/**
* Recurses through the two given types and returns an array of tuples
* that map type variable names to the type to substitute.
*/
function getTypeVariableMappings(node, actualType, expectedType) {
// If type variable mapping is found, terminate early
if (expectedType.kind === 'variable') {
return [[expectedType.name, actualType]];
}
// If actual type is a type reference, expand type first
if (actualType.kind === 'variable') {
actualType = lookupTypeAliasAndRemoveForAllTypes(node, actualType);
}
const mappings = [];
switch (expectedType.kind) {
case 'pair':
if (actualType.kind === 'list') {
if (actualType.typeAsPair !== undefined) {
mappings.push(...getTypeVariableMappings(node, actualType.typeAsPair.headType, expectedType.headType));
mappings.push(...getTypeVariableMappings(node, actualType.typeAsPair.tailType, expectedType.tailType));
}
else {
mappings.push(...getTypeVariableMappings(node, actualType.elementType, expectedType.headType));
mappings.push(...getTypeVariableMappings(node, actualType.elementType, expectedType.tailType));
}
}
if (actualType.kind === 'pair') {
mappings.push(...getTypeVariableMappings(node, actualType.headType, expectedType.headType));
mappings.push(...getTypeVariableMappings(node, actualType.tailType, expectedType.tailType));
}
break;
case 'list':
if (actualType.kind === 'list') {
mappings.push(...getTypeVariableMappings(node, actualType.elementType, expectedType.elementType));
}
break;
case 'function':
if (actualType.kind === 'function' &&
actualType.parameterTypes.length === expectedType.parameterTypes.length) {
for (let i = 0; i < actualType.parameterTypes.length; i++) {
mappings.push(...getTypeVariableMappings(node, actualType.parameterTypes[i], expectedType.parameterTypes[i]));
}
mappings.push(...getTypeVariableMappings(node, actualType.returnType, expectedType.returnType));
}
break;
default:
break;
}
return mappings;
}
/**
* Checks if the actual type matches the expected type.
* If not, adds type mismatch error to context.
*/
function checkForTypeMismatch(node, actualType, expectedType) {
const formatAsLiteral = typeContainsLiteralType(expectedType) || typeContainsLiteralType(actualType);
if (hasTypeMismatchErrors(node, actualType, expectedType)) {
context.errors.push(new typeErrors_1.TypeMismatchError(node, (0, utils_1.formatTypeString)(actualType, formatAsLiteral), (0, utils_1.formatTypeString)(expectedType, formatAsLiteral)));
}
}
/**
* Returns true if given type contains literal type, false otherwise.
* This is necessary to determine whether types should be formatted as
* literal type or primitive type in error messages.
*/
function typeContainsLiteralType(type) {
switch (type.kind) {
case 'primitive':
case 'variable':
return false;
case 'literal':
return true;
case 'function':
return (typeContainsLiteralType(type.returnType) ||
type.parameterTypes.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false));
case 'union':
return type.types.reduce((prev, curr) => prev || typeContainsLiteralType(curr), false);
default:
return false;
}
}
/**
* Returns true if the actual type and the expected type do not match, false otherwise.
* The two types will not match if the intersection of the two types is empty.
*
* @param node Current node being checked
* @param actualType Type being checked
* @param expectedType Type the actual type is being checked against
* @param visitedTypeAliasesForActualType Array that keeps track of previously encountered type aliases
* for actual type to prevent infinite recursion
* @param visitedTypeAliasesForExpectedType Array that keeps track of previously encountered type aliases
* for expected type to prevent infinite recursion
* @param skipTypeAliasExpansion If true, type aliases are not expanded (e.g. in type alias declarations)
* @returns true if the actual type and the expected type do not match, false otherwise
*/
function hasTypeMismatchErrors(node, actualType, expectedType, visitedTypeAliasesForActualType = [], visitedTypeAliasesForExpectedType = [], skipTypeAliasExpansion = false) {
if ((0, lodash_1.isEqual)(actualType, utils_1.tAny) || (0, lodash_1.isEqual)(expectedType, utils_1.tAny)) {
// Exit early as "any" is guaranteed not to cause type mismatch errors
return false;
}
if (expectedType.kind !== 'variable' && actualType.kind === 'variable') {
// If the expected type is not a variable type but the actual type is a variable type,
// Swap the order of the types around
// This removes the need to check if the actual type is a variable type in all of the switch cases
return hasTypeMismatchErrors(node, expectedType, actualType, visitedTypeAliasesForExpectedType, visitedTypeAliasesForActualType, skipTypeAliasExpansion);
}
if (expectedType.kind !== 'union' && actualType.kind === 'union') {
// If the expected type is not a union type but the actual type is a union type,
// Check if the expected type matches any of the actual types
// This removes the need to check if the actual type is a union type in all of the switch cases
return !containsType(node, actualType.types, expectedType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType);
}
switch (expectedType.kind) {
case 'variable':
if (actualType.kind === 'variable') {
// If both are variable types, types match if both name and type arguments match
if (expectedType.name === actualType.name) {
if (expectedType.typeArgs === undefined || expectedType.typeArgs.length === 0) {
return actualType.typeArgs === undefined ? false : actualType.typeArgs.length !== 0;
}
if (actualType.typeArgs?.length !== expectedType.typeArgs.length) {
return true;
}
for (let i = 0; i < expectedType.typeArgs.length; i++) {
if (hasTypeMismatchErrors(node, actualType.typeArgs[i], expectedType.typeArgs[i], visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion)) {
return true;
}
}
return false;
}
}
for (const visitedType of visitedTypeAliasesForExpectedType) {
if ((0, lodash_1.isEqual)(visitedType, expectedType)) {
// Circular dependency, treat as type mismatch
return true;
}
}
// Skips expansion, treat as type mismatch
if (skipTypeAliasExpansion) {
return true;
}
visitedTypeAliasesForExpectedType.push(expectedType);
// Expand type and continue typechecking
const aliasType = lookupTypeAliasAndRemoveForAllTypes(node, expectedType);
return hasTypeMismatchErrors(node, actualType, aliasType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
case 'primitive':
if (actualType.kind === 'literal') {
return expectedType.value === undefined
? typeof actualType.value !== expectedType.name
: actualType.value !== expectedType.value;
}
if (actualType.kind !== 'primitive') {
return true;
}
return actualType.name !== expectedType.name;
case 'function':
if (actualType.kind !== 'function') {
return true;
}
// Check parameter types
const actualParamTypes = actualType.parameterTypes;
const expectedParamTypes = expectedType.parameterTypes;
if (actualParamTypes.length !== expectedParamTypes.length) {
return true;
}
for (let i = 0; i < actualType.parameterTypes.length; i++) {
// Note that actual and expected types are swapped here
// to simulate contravariance for function parameter types
// This will be useful if type checking in Source Typed were to be made stricter in the future
if (hasTypeMismatchErrors(node, expectedParamTypes[i], actualParamTypes[i], visitedTypeAliasesForExpectedType, visitedTypeAliasesForActualType, skipTypeAliasExpansion)) {
return true;
}
}
// Check return type
return hasTypeMismatchErrors(node, actualType.returnType, expectedType.returnType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
case 'union':
// If actual type is not union type, check if actual type matches one of the expected types
if (actualType.kind !== 'union') {
return !containsType(node, expectedType.types, actualType);
}
// If both are union types, there are no type errors as long as one of the types match
for (const type of actualType.types) {
if (containsType(node, expectedType.types, type)) {
return false;
}
}
return true;
case 'literal':
if (actualType.kind !== 'literal' && actualType.kind !== 'primitive') {
return true;
}
if (actualType.kind === 'primitive' && actualType.value === undefined) {
return actualType.name !== typeof expectedType.value;
}
return actualType.value !== expectedType.value;
case 'pair':
if (actualType.kind === 'list') {
// Special case, as lists are pairs
if (actualType.typeAsPair !== undefined) {
// If pair representation of list is present, check against pair type
return hasTypeMismatchErrors(node, actualType.typeAsPair, expectedType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
}
// Head of pair should match list element type; tail of pair should match list type
return (hasTypeMismatchErrors(node, actualType.elementType, expectedType.headType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion) ||
hasTypeMismatchErrors(node, actualType, expectedType.tailType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion));
}
if (actualType.kind !== 'pair') {
return true;
}
return (hasTypeMismatchErrors(node, actualType.headType, expectedType.headType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion) ||
hasTypeMismatchErrors(node, actualType.tailType, expectedType.tailType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion));
case 'list':
if ((0, lodash_1.isEqual)(actualType, utils_1.tNull)) {
// Null matches against any list type as null is empty list
return false;
}
if (actualType.kind === 'pair') {
// Special case, as pairs can be lists
if (expectedType.typeAsPair !== undefined) {
// If pair representation of list is present, check against pair type
return hasTypeMismatchErrors(node, actualType, expectedType.typeAsPair, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
}
// Head of pair should match list element type; tail of pair should match list type
return (hasTypeMismatchErrors(node, actualType.headType, expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion) ||
hasTypeMismatchErrors(node, actualType.tailType, expectedType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion));
}
if (actualType.kind !== 'list') {
return true;
}
return hasTypeMismatchErrors(node, actualType.elementType, expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
case 'array':
if (actualType.kind === 'union') {
// Special case: number[] | string[] matches with (number | string)[]
const types = actualType.types.filter((type) => type.kind === 'array');
if (types.length !== actualType.types.length) {
return true;
}
const combinedType = types.map(type => type.elementType);
return hasTypeMismatchErrors(node, (0, utils_1.tUnion)(...combinedType), expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
}
if (actualType.kind !== 'array') {
return true;
}
return hasTypeMismatchErrors(node, actualType.elementType, expectedType.elementType, visitedTypeAliasesForActualType, visitedTypeAliasesForExpectedType, skipTypeAliasExpansion);
default:
return true;
}
}
/**
* Converts type annotation/type alias declaration node to its corresponding type representation in Source.
* If no type annotation exists, returns the "any" primitive type.
*/
function getTypeAnnotationType(annotationNode) {
if (!annotationNode) {
return utils_1.tAny;
}
return getAnnotatedType(annotationNode.typeAnnotation);
}
/**
* Converts type node to its corresponding type representation in Source.
*/
function getAnnotatedType(typeNode) {
switch (typeNode.type) {
case 'TSFunctionType':
const params = typeNode.parameters;
// If the function has variable number of arguments, set function type as any
// TODO: Add support for variable number of function arguments
const hasVarArgs = params.reduce((prev, curr) => prev || curr.type === 'RestElement', false);
if (hasVarArgs) {
return utils_1.tAny;
}
const fnTypes = getParamTypes(params);
// Return type will always be last item in types array
fnTypes.push(getTypeAnnotationType(typeNode.typeAnnotation));
return (0, utils_1.tFunc)(...fnTypes);
case 'TSLiteralType':
const value = typeNode.literal.value;
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {