diff --git a/src/common.ts b/src/common.ts index da7e63b24..b0413825a 100644 --- a/src/common.ts +++ b/src/common.ts @@ -20,6 +20,7 @@ import {TextSpan, TextPos, TextFile} from "./text"; import {Scanner} from "./scanner"; import * as path from "path" import { convertNodeToSymbolPath } from "./resolver"; +import { TYPE_ERROR_MESSAGES } from "./diagnostics"; export function getSourceFile(node: Syntax) { while (true) { @@ -250,6 +251,19 @@ export function isExported(node: Syntax) { } } +export function hasTypeError(node: Syntax) { + for (const message of TYPE_ERROR_MESSAGES) { + if (hasDiagnostic(node, message)) { + return true; + } + } + return false; +} + +export function hasDiagnostic(node: Syntax, message: string): boolean { + return node.errors.some(d => d.message === message); +} + export function getFullyQualifiedPathToNode(node: Syntax) { const symbolPath = convertNodeToSymbolPath(node); while (true) { diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 5623128e3..07e85c019 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -17,7 +17,7 @@ export const E_TYPE_MISMATCH = "Types {left} and {right} are not semantically eq export const E_TOO_FEW_ARGUMENTS_FOR_FUNCTION_CALL = "Too few arguments for function call. Expected {expected} but got {actual}."; export const E_TOO_MANY_ARGUMENTS_FOR_FUNCTION_CALL = "Too many arguments for function call. Expected {expected} but got {actual}."; export const E_CANDIDATE_FUNCTION_REQUIRES_THIS_PARAMETER = "Candidate function requires this parameter." -export const E_ARGUMENT_HAS_NO_CORRESPONDING_PARAMETER = "Argument has no corresponding parameter." +export const E_ARGUMENT_HAS_NO_CORRESPONDING_PARAMETER = "This argument is missing a corresponding parameter." export const E_INVALID_ARGUMENTS = "Invalid arguments passed to function '{name}'" export const E_RECORD_MISSING_MEMBER = "Record {name} does not have a member declaration named {memberName}" export const E_TYPES_MISSING_MEMBER = "Not all types resolve to a record with the a member named '{name}'." @@ -26,6 +26,13 @@ export const E_MAY_NOT_RETURN_BECAUSE_TYPE_RESOLVES_TO_VOID = "May not return a export const E_MUST_RETURN_BECAUSE_TYPE_DOES_NOT_RESOLVE_TO_VOID = "Must return a value because the function's return type does not resolve to '()'" export const E_ARGUMENT_TYPE_NOT_ASSIGNABLE = "This argument's type is not assignable to the function's parameter type." +export const TYPE_ERROR_MESSAGES = [ + E_TOO_FEW_ARGUMENTS_FOR_FUNCTION_CALL, + E_TOO_MANY_ARGUMENTS_FOR_FUNCTION_CALL, + E_ARGUMENT_TYPE_NOT_ASSIGNABLE, + E_TYPE_MISMATCH, +] + const BOLT_HARD_ERRORS = process.env['BOLT_HARD_ERRORS'] export interface Diagnostic { @@ -52,6 +59,8 @@ export class DiagnosticPrinter { public hasErrors = false public hasFatal = false; + private indent = 0; + public add(diagnostic: Diagnostic): void { if (BOLT_HARD_ERRORS && (diagnostic.severity === 'error' || diagnostic.severity === 'fatal')) { @@ -64,7 +73,9 @@ export class DiagnosticPrinter { throw new Error(out); } - let out = '' + const indentation = ' '.repeat(this.indent); + + let out = indentation; switch (diagnostic.severity) { case 'error': @@ -78,6 +89,9 @@ export class DiagnosticPrinter { this.hasErrors = true; out += chalk.bold.red('warning: '); break; + case 'info': + out += chalk.bold.yellow('info: ') + break; default: throw new Error(`Unkown severity for diagnostic message.`); } @@ -103,8 +117,8 @@ export class DiagnosticPrinter { for (let i = startLine; i < endLine; i++) { const line = lines[i]; let j = firstIndexOfNonEmpty(line); - out += ' '+chalk.bgWhite.black(' '.repeat(gutterWidth-countDigits(i+1))+(i+1).toString())+' '+line+'\n' - const gutter = ' '+chalk.bgWhite.black(' '.repeat(gutterWidth))+' ' + out += indentation + ' '+chalk.bgWhite.black(' '.repeat(gutterWidth-countDigits(i+1))+(i+1).toString())+' '+line+'\n' + const gutter = indentation + ' '+chalk.bgWhite.black(' '.repeat(gutterWidth))+' ' let mark: number; let skip: number; if (i === span.start.line-1 && i === span.end.line-1) { @@ -131,6 +145,15 @@ export class DiagnosticPrinter { } process.stderr.write(out); + + if (diagnostic.nested !== undefined) { + this.indent += 2; + for (const nested of diagnostic.nested) { + this.add(nested); + } + this.indent -= 2; + } + } } diff --git a/src/test/types.ts b/src/test/types.ts index 691d29793..59fcbaa4a 100644 --- a/src/test/types.ts +++ b/src/test/types.ts @@ -11,7 +11,7 @@ describe('a function that merges two equivalent types', () => { type1.node = createBoltIdentifier('a'); const type2 = new AnyType; type2.node = createBoltIdentifier('b'); - const types = new UnionType([type1 type2]); + const types = new UnionType([type1, type2]); mergeTypes(types); }) diff --git a/src/types.ts b/src/types.ts index 61a911f5a..44b7077e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import { FastStringMap, assert, isPlainObject, some, prettyPrintTag, map, flatMa import { SyntaxKind, Syntax, isBoltTypeExpression, BoltExpression, BoltFunctionDeclaration, BoltFunctionBodyElement, kindToString, SourceFile, isBoltExpression, BoltCallExpression, BoltIdentifier, isBoltDeclarationLike, isBoltPattern, isJSExpression, isBoltStatement, isJSStatement, isJSPattern, isJSParameter, isBoltParameter, isBoltMatchArm, isBoltRecordField, isBoltRecordFieldPattern, isEndOfFile, isSyntax, } from "./ast"; import { convertNodeToSymbolPath, ScopeType, SymbolResolver, SymbolInfo, SymbolPath } from "./resolver"; import { Value, Record } from "./evaluator"; -import { getReturnStatementsInFunctionBody, getAllReturnStatementsInFunctionBody, getFullyQualifiedPathToNode } from "./common"; +import { getReturnStatementsInFunctionBody, getAllReturnStatementsInFunctionBody, getFullyQualifiedPathToNode, hasDiagnostic, hasTypeError } from "./common"; import { E_TOO_MANY_ARGUMENTS_FOR_FUNCTION_CALL, E_TOO_FEW_ARGUMENTS_FOR_FUNCTION_CALL, E_CANDIDATE_FUNCTION_REQUIRES_THIS_PARAMETER, E_ARGUMENT_HAS_NO_CORRESPONDING_PARAMETER, E_TYPE_MISMATCH, Diagnostic, E_ARGUMENT_TYPE_NOT_ASSIGNABLE } from "./diagnostics"; import { BOLT_MAX_FIELDS_TO_PRINT } from "./constants"; import { emitNode } from "./emitter"; @@ -147,7 +147,7 @@ export class OpaqueType extends TypeBase { public kind: TypeKind.OpaqueType = TypeKind.OpaqueType; - constructor(public name: string) { + constructor(public name: string, public source?: Syntax) { super(); } @@ -335,6 +335,7 @@ export function prettyPrintType(type: Type): string { out += '{ ' + elementType.name + ' } '; break; case TypeKind.ReturnType: + out += 'return type of ' out += prettyPrintType(elementType.fnType); out += '('; out += elementType.argumentTypes.map(prettyPrintType).join(', ') @@ -433,10 +434,8 @@ function introducesType(node: Syntax) { || isBoltRecordField(node) || isBoltRecordFieldPattern(node) || isBoltPattern(node) - || isBoltStatement(node) || isBoltTypeExpression(node) || isJSExpression(node) - || isJSStatement(node) || isJSPattern(node) || isJSParameter(node) } @@ -487,78 +486,8 @@ export class TypeChecker { } } - private diagnoseTypeMismatch(node: Syntax, a: Type, b: Type): void { - if (a.kind === TypeKind.ReturnType && b.kind === TypeKind.FunctionType) { - if (b.paramTypes.length > a.argumentTypes.length) { - let nested: Diagnostic[] = []; - //for (let i = b.paramTypes.length; i < a.argumentTypes.length; i++) { - // nested.push({ - // message: E_CANDIDATE_FUNCTION_REQUIRES_THIS_PARAMETER, - // severity: 'error', - // node: b.getTypeAtParameterIndex(a.argumentTypes.length).node! - // }); - //} - node.errors.push({ - message: E_TOO_MANY_ARGUMENTS_FOR_FUNCTION_CALL, - severity: 'error', - node: a.node, - args: { - expected: b.paramTypes.length, - actual: a.argumentTypes.length, - }, - nested, - }); - } else if (b.paramTypes.length < a.argumentTypes.length) { - let nested: Diagnostic[] = []; - //for (let i = b.paramTypes.length; i < a.argumentTypes.length; i++) { - // nested.push({ - // message: E_ARGUMENT_HAS_NO_CORRESPONDING_PARAMETER, - // severity: 'error', - // node: (a.node as BoltCallExpression).operands[i] - // }); - //} - node.errors.push({ - message: E_TOO_FEW_ARGUMENTS_FOR_FUNCTION_CALL, - severity: 'error', - node: a.node, - args: { - expected: b.paramTypes.length, - actual: a.argumentTypes.length, - }, - nested, - }); - } else { - const paramCount = a.argumentTypes.length; - for (let i = 0; i < paramCount; i++) { - const argType = a.argumentTypes[i]; - const paramType = b.paramTypes[i]; - if (!this.areTypesSemanticallyEquivalent(argType, paramType)) { - node.errors.push({ - message: E_ARGUMENT_TYPE_NOT_ASSIGNABLE, - severity: 'error', - node: argType.node, - }) - } - //if (!this.areTypesSemanticallyEquivalent(paramA, paramB)) { - // yield { - // message: E_TYPE_MISMATCH, - // severity: 'error', - // node: a.node, - // args: { - // left: a, - // right: b, - // }, - // }; - //} - } - } - } else { - node.errors.push({ - message: E_TYPE_MISMATCH, - severity: 'error', - args: { left: a, right: b }, - }) - } + private *diagnoseTypeMismatch(a: Type, b: Type): IterableIterator { + } public registerSourceFile(sourceFile: SourceFile): void { @@ -607,6 +536,7 @@ export class TypeChecker { const node = queued.shift()!; const derivedType = this.deriveTypeUsingChildren(node); + derivedType.node = node; const newType = this.simplifyType(derivedType); //if (newType !== derivedType) { @@ -616,13 +546,14 @@ export class TypeChecker { const narrowedType = this.narrowTypeDownTo(node.type!.solved, newType); if (narrowedType === null) { - this.diagnoseTypeMismatch(node, node.type!, derivedType); - //node.errors.push({ - // message: E_TYPE_MISMATCH, - // severity: 'error', - // args: { left: node.type!, right: newType }, - // nested: [...this.diagnoseTypeMismatch(node.type!.solved, newType)], - //}); + if (!hasTypeError(node)) { + node.errors.push({ + message: E_TYPE_MISMATCH, + severity: 'error', + args: { left: node.type!, right: newType }, + nested: [...this.diagnoseTypeMismatch(node.type!.solved, newType)], + }); + } } else { narrowedType.node = node; if (node.type.solved !== narrowedType) { @@ -672,87 +603,6 @@ export class TypeChecker { return this.areTypesSemanticallyEquivalent(new TupleType, type); } - //private *getTypesForMember(origNode: Syntax, fieldName: string, type: Type): IterableIterator { - // switch (type.kind) { - // case TypeKind.UnionType: - // { - // const typesMissingMember = []; - // for (const elementType of getAllPossibleElementTypes(type)) { - // let foundType = false; - // for (const recordType of this.getTypesForMemberNoUnionType(origNode, fieldName, elementType, false)) { - // yield recordType; - // foundType = true; - // } - // if (!foundType) { - // origNode.errors.push({ - // message: E_TYPES_MISSING_MEMBER, - // severity: 'error', - // }) - // } - // } - // } - // default: - // return this.getTypesForMemberNoUnionType(origNode, fieldName, type, true); - // } - //} - - //private *getTypesForMemberNoUnionType(origNode: Syntax, fieldName: string, type: Type, hardError: boolean): IterableIterator { - // switch (type.kind) { - // case TypeKind.AnyType: - // break; - // case TypeKind.FunctionType: - // if (hardError) { - // origNode.errors.push({ - // message: E_TYPES_MISSING_MEMBER, - // severity: 'error', - // args: { - // name: fieldName, - // }, - // nested: [{ - // message: E_NODE_DOES_NOT_CONTAIN_MEMBER, - // severity: 'error', - // node: type.node!, - // }] - // }); - // } - // break; - // case TypeKind.RecordType: - // { - // if (type.isFieldRequired(fieldName)) { - // const fieldType = type.getAllMemberTypesForField(fieldName); - // yield (fieldType as PlainRecordFieldType).type; - // } else { - // if (hardError) { - // origNode.errors.push({ - // message: E_TYPES_MISSING_MEMBER, - // severity: 'error', - // args: { - // name: fieldName - // } - // }) - // } - // } - // break; - // } - // default: - // throw new Error(`I do not know how to find record member types for ${TypeKind[type.kind]}`) - // } - //} - - //private *getAllNodesForType(type: Type): IterableIterator { - // if (type.node !== undefined) { - // yield type.node; - // } - // switch (type.kind) { - // case TypeKind.UnionType: - // for (const elementType of type.getElementTypes()) { - // yield* this.getAllNodesForType(type); - // } - // break; - // default: - // } - //} - /** * Narrows @param outter down to @param inner. If @param outer could not be narrowed, this function return null. * @@ -836,36 +686,38 @@ export class TypeChecker { return new AnyType; } + this.mergeDuplicateTypes(elementTypes); + // This is a small optimisation to prevent the complexity of processing a // union type everwhere where it just contains one element. if (elementTypes.length === 1) { return elementTypes[0]; } - return this.mergeDuplicateTypes(new UnionType(elementTypes)); + return new UnionType(elementTypes); } private deriveTypeUsingChildren(node: Syntax): Type { switch (node.kind) { - case SyntaxKind.JSReturnStatement: - { - if (node.value === null) { - return this.getOpaqueType('undefined'); - } - this.markNodeAsRequiringUpdate(node.value, node); - return node.value.type!.solved; - } + //case SyntaxKind.JSReturnStatement: + //{ + // if (node.value === null) { + // return this.getOpaqueType('undefined'); + // } + // this.markNodeAsRequiringUpdate(node.value, node); + // return node.value.type!.solved; + //} - case SyntaxKind.JSExpressionStatement: - { - if (node.expression === null) { - return new TupleType; - } - this.markNodeAsRequiringUpdate(node.expression, node); - return node.expression.type!.solved; - } + //case SyntaxKind.JSExpressionStatement: + //{ + // if (node.expression === null) { + // return new TupleType; + // } + // this.markNodeAsRequiringUpdate(node.expression, node); + // return node.expression.type!.solved; + //} case SyntaxKind.JSMemberExpression: { @@ -947,7 +799,7 @@ export class TypeChecker { { if (node.members === null) { const symbolPath = getFullyQualifiedPathToNode(node); - return new OpaqueType(symbolPath.encode()); + return new OpaqueType(symbolPath.encode(), node); } else { let memberTypes: RecordFieldType[] = [] for (const member of node.members) { @@ -1069,31 +921,33 @@ export class TypeChecker { return this.getOpaqueType('Lang::Bolt::Node'); } - case SyntaxKind.BoltExpressionStatement: - { - return this.deriveTypeUsingChildren(node.expression); - } + //case SyntaxKind.BoltExpressionStatement: + //{ + // return this.deriveTypeUsingChildren(node.expression); + //} - case SyntaxKind.BoltReturnStatement: - { - if (node.value === null) { - const tupleType = new TupleType(); - return tupleType - } - return node.value.type!.solved; - } + //case SyntaxKind.BoltReturnStatement: + //{ + // if (node.value === null) { + // const tupleType = new TupleType(); + // return tupleType + // } + // return node.value.type!.solved; + //} - case SyntaxKind.BoltBlockExpression: - { - let elementTypes = []; - if (node.elements !== null) { - for (const returnStmt of getReturnStatementsInFunctionBody(node.elements)) { - elementTypes.push(returnStmt.type!.solved) - this.markNodeAsRequiringUpdate(returnStmt, node); - } - } - return new UnionType(elementTypes); - } + //case SyntaxKind.BoltBlockExpression: + //{ + // let elementTypes = []; + // if (node.elements !== null) { + // for (const returnStmt of getReturnStatementsInFunctionBody(node.elements)) { + // if (returnStmt.value !== null) { + // elementTypes.push(returnStmt.value.type!.solved) + // this.markNodeAsRequiringUpdate(returnStmt.value, node); + // } + // } + // } + // return new UnionType(elementTypes); + //} case SyntaxKind.BoltMemberExpression: { @@ -1145,8 +999,12 @@ export class TypeChecker { } if (node.body !== null) { for (const returnStmt of getAllReturnStatementsInFunctionBody(node.body)) { - returnTypes.push(returnStmt.type!.solved); - this.markNodeAsRequiringUpdate(returnStmt, node); + if (returnStmt.value !== null) { + returnTypes.push(returnStmt.value.type!.solved); + this.markNodeAsRequiringUpdate(returnStmt.value, node); + } else { + returnTypes.push(new TupleType); + } } } let paramTypes = []; @@ -1261,7 +1119,7 @@ export class TypeChecker { return this.areTypesSemanticallyEquivalent(resolvedType, b); } if (b.kind === TypeKind.ReturnType) { - const resolvedType = this.resolveReturnType(a); + const resolvedType = this.resolveReturnType(b); return this.areTypesSemanticallyEquivalent(a, resolvedType); } @@ -1282,7 +1140,7 @@ export class TypeChecker { // FIXME There are probably more cases that should be covered. throw new Error(`I did not know how to calculate the equivalence of ${TypeKind[a.kind]} and ${TypeKind[b.kind]}`) } - + private resolveReturnType(type: ReturnType): Type { let resultTypes = []; @@ -1306,38 +1164,91 @@ export class TypeChecker { case TypeKind.FunctionType: - // Early check to see if the signature of the function call matches that of the function declaration. - if (type.argumentTypes.length !== elementType.paramTypes.length) { - continue; - } + if (elementType.paramTypes.length < type.argumentTypes.length) { - // Since we are assured by now that `type.argumentTypes` and `elementType.paramTypes` have equal length, - // we may pick the length of any of these two as the index bound in the next loop. - const paramCount = type.argumentTypes.length; + if (!hasDiagnostic(type.node!, E_TOO_MANY_ARGUMENTS_FOR_FUNCTION_CALL)) { - let matchFailed = false; + let nested: Diagnostic[] = []; + for (let i = elementType.paramTypes.length; i < type.argumentTypes.length; i++) { + nested.push({ + message: E_CANDIDATE_FUNCTION_REQUIRES_THIS_PARAMETER, + severity: 'error', + node: elementType.getTypeAtParameterIndex(type.argumentTypes.length).node! + }); + } + type.node!.errors.push({ + message: E_TOO_MANY_ARGUMENTS_FOR_FUNCTION_CALL, + severity: 'error', + args: { + expected: elementType.paramTypes.length, + actual: type.argumentTypes.length, + }, + nested, + }); - // Iterate pairwise over the argument types and parameter types, checking whether they are assignable to each other. - // In the current version of the checker, this amounts to checking if they are semantically equal. - for (let i = 0; i < paramCount; i++) { - const argType = type.argumentTypes[i].solved; - const paramType = elementType.paramTypes[i].solved; - if (!this.areTypesSemanticallyEquivalent(argType, paramType)) { - //argType.node!.errors.push({ - //message: E_TYPE_MISMATCH, - //severity: 'error', - //args: { left: paramType, right: argType } - //}) - matchFailed = true; - break; } + + // Skip this return type + continue; + + } else if (elementType.paramTypes.length > type.argumentTypes.length) { + + if (!hasDiagnostic(type.node!, E_TOO_FEW_ARGUMENTS_FOR_FUNCTION_CALL)) { + + const nested: Diagnostic[] = []; + for (let i = type.argumentTypes.length; i < elementType.paramTypes.length; i++) { + nested.push({ + message: E_ARGUMENT_HAS_NO_CORRESPONDING_PARAMETER, + severity: 'info', + node: elementType.paramTypes[i].node! + }); + } + type.node!.errors.push({ + message: E_TOO_FEW_ARGUMENTS_FOR_FUNCTION_CALL, + severity: 'error', + args: { + expected: elementType.paramTypes.length, + actual: type.argumentTypes.length, + }, + nested, + }); + + } + + // Skip this return type + continue; + + } else { + + let hasErrors = false; + + const paramCount = type.argumentTypes.length; + for (let i = 0; i < paramCount; i++) { + const argType = type.argumentTypes[i]; + const paramType = elementType.paramTypes[i]; + if (!this.areTypesSemanticallyEquivalent(argType, paramType)) { + if (!hasDiagnostic(type.solved.node!, E_ARGUMENT_TYPE_NOT_ASSIGNABLE)) { + type.node!.errors.push({ + message: E_ARGUMENT_TYPE_NOT_ASSIGNABLE, + severity: 'error', + node: argType.node, + args: { argType, paramType } + }) + } + hasErrors = true; + } + } + + // Skip this return type if we had type errors + if (hasErrors) { + continue; + } + } // If the argument types and parameter types didn't fail to match, // the function type is eligable to be 'called' by the given ReturnType. - if (!matchFailed) { - resultTypes.push(elementType.returnType.solved); - } + resultTypes.push(elementType.returnType.solved); break; default: @@ -1363,9 +1274,7 @@ export class TypeChecker { return new UnionType(resultTypes); } - private mergeDuplicateTypes(type: Type): Type { - - const resultTypes = [...getAllPossibleElementTypes(type)]; + private mergeDuplicateTypes(resultTypes: Type[]): void { resultTypes.sort(comparator(areTypesLexicallyLessThan)); @@ -1377,19 +1286,9 @@ export class TypeChecker { break; } } - resultTypes.splice(i, j-i-1); resultTypes.splice(i+1, j-i-1); } - - if (resultTypes.length === 0) { - return new NeverType; - } - - if (resultTypes.length === 1) { - return resultTypes[0]; - } - - return new UnionType(resultTypes); + } private simplifyType(type: Type): Type { @@ -1403,8 +1302,6 @@ export class TypeChecker { // iteration. const stack = [ type ]; - //this.removeDuplicateTypes(elementTypes); - while (stack.length > 0) { const elementType = stack.pop()!; @@ -1454,12 +1351,14 @@ export class TypeChecker { } } + this.mergeDuplicateTypes(resultTypes); + // Small optimisation to make debugging easier. if (resultTypes.length === 1) { return resultTypes[0] } - return this.mergeDuplicateTypes(new UnionType(resultTypes)); + return new UnionType(resultTypes); } }