UNPKG

derw

Version:

An Elm-inspired language that transpiles to TypeScript

1,773 lines (1,588 loc) 81.8 kB
import { Err, Ok, Result } from "@eeue56/ts-core/build/main/lib/result"; import { isBuiltinType } from "./builtins"; import { suggestName } from "./errors/distance"; import { generateExpression } from "./generators/Derw"; import { Addition, And, Block, Branch, CaseStatement, Const, Constructor, Division, Equality, Expression, FixedType, FormatStringValue, Function, FunctionCall, FunctionType, GenericType, GreaterThan, GreaterThanOrEqual, IfStatement, Import, InEquality, Lambda, LambdaCall, LeftPipe, LessThan, LessThanOrEqual, ListPrepend, ListRange, ListValue, Mod, ModuleReference, Multiplication, ObjectLiteral, ObjectLiteralType, Or, Property, RightPipe, StringValue, Subtraction, Tag, TagArg, Type, TypeAlias, TypedBlock, UnionType, UnionUntaggedType, Value, } from "./types"; type ScopedValues = Record<string, Type>; function isSameGenericType( first: GenericType, second: GenericType, topLevel: boolean ): boolean { if (topLevel) return true; // todo: figure this out //return first.name === second.name; return true; } function isSameFixedType( first: FixedType, second: FixedType, topLevel: boolean ): boolean { if ( (first.name === "any" && first.args.length === 0) || (second.name === "any" && second.args.length === 0) ) { return true; } if (first.args.length !== second.args.length) { return false; } if (first.name !== second.name) return false; for (var i = 0; i < first.args.length; i++) { if (!isSameType(first.args[i], second.args[i], topLevel)) { return false; } } return true; } function isSameFunctionType( first: FunctionType, second: FunctionType, topLevel: boolean ): boolean { if (first.args.length !== second.args.length) { return false; } for (var i = 0; i < first.args.length; i++) { if (!isSameType(first.args[i], second.args[i], topLevel)) { return false; } } return true; } function doesFunctionTypeContainType( first: FunctionType, second: GenericType | FixedType, topLevel: boolean ): boolean { switch (second.kind) { case "GenericType": { for (const arg of first.args) { if (isSameType(arg, second, topLevel)) { return true; } } } case "FixedType": { for (const arg of first.args) { if (isSameType(arg, second, topLevel)) { return true; } } } } return false; } function isSameObjectLiteralType( first: ObjectLiteralType, second: ObjectLiteralType ): boolean { const processedNames = [ ]; for (const firstPropertyName of Object.keys(first.properties)) { if (second.properties[firstPropertyName]) { // when the types don't match between first and second if ( !isSameType( first.properties[firstPropertyName], second.properties[firstPropertyName], false ) ) { return false; } processedNames.push(firstPropertyName); } else { // when one property exists on first but not second return false; } } for (const secondPropertyName of Object.keys(second.properties)) { if (!processedNames.includes(secondPropertyName)) { // when one property exists on second but not first return false; } } return true; } function isSameObjectLiteralTypeAlias( objectLiteral: ObjectLiteralType, expectedType: Type, typedBlocks: TypedBlock[] ): boolean { if (expectedType.kind === "GenericType") return true; const expectedTypeAlias = getTypeAlias(expectedType, typedBlocks); if (expectedTypeAlias.kind === "Err") return false; const typeAlias = expectedTypeAlias.value; const processedNames = [ ]; for (const property of typeAlias.properties) { if (objectLiteral.properties[property.name]) { if ( !isSameType( property.type, objectLiteral.properties[property.name], false ) ) { return false; } else { processedNames.push(property.name); } } else { return false; } } for (const property of Object.keys(objectLiteral.properties)) { if (!processedNames.includes(property)) { // when one property exists on second but not first return false; } } return true; } function tagToFixedType(tag: Tag): FixedType { return FixedType( tag.name, tag.args .filter((tag) => tag.type.kind === "GenericType") .map((arg) => arg.type) ); } function isATag(type_: Type, typedBlocks: TypedBlock[]): Result<null, Type> { for (const block of typedBlocks) { switch (block.kind) { case "UnionType": { for (const tag of block.tags) { const tagType = tagToFixedType(tag); if (isSameType(type_, tagType, false)) return Ok(block.type); } } } } return Err(null); } export function isSameType( first: Type, second: Type, topLevel: boolean, typedBlocks: TypedBlock[] = [ ] ): boolean { if ( first.kind === "ObjectLiteralType" && second.kind === "ObjectLiteralType" ) { return isSameObjectLiteralType(first, second); } if ( (first.kind !== "FunctionType" && first.kind !== "ObjectLiteralType" && first.name === "any") || (second.kind !== "FunctionType" && second.kind !== "ObjectLiteralType" && second.name === "any") ) { return true; } if ( (first.kind === "ObjectLiteralType" && second.kind === "GenericType") || (second.kind === "ObjectLiteralType" && first.kind === "GenericType") ) { return true; } if ( first.kind === "ObjectLiteralType" || second.kind === "ObjectLiteralType" ) { return false; } if (first.kind !== second.kind) { if (first.kind === "FunctionType" && second.kind !== "FunctionType") { return doesFunctionTypeContainType(first, second, topLevel); } if (first.kind === "FixedType" && second.kind === "GenericType") { return true; } if (second.kind === "FixedType" && first.kind === "GenericType") { return true; } return false; } switch (first.kind) { case "FixedType": { if (first.name.indexOf(".") > -1) { const split = first.name.split("."); first = { ...first, name: split[split.length - 1], }; } second = second as FixedType; if (second.name.indexOf(".") > -1) { const split = second.name.split("."); second = { ...second, name: split[split.length - 1], }; } if (isSameFixedType(first, second, topLevel)) { return true; } const isFirstATag = isATag(first, typedBlocks); const isSecondATag = isATag(second, typedBlocks); if (isFirstATag.kind === "Ok") { if ( isSameType(isFirstATag.value, second, topLevel, typedBlocks) ) { return true; } } if (isSecondATag.kind === "Ok") { if ( isSameType(first, isSecondATag.value, topLevel, typedBlocks) ) { return true; } } return false; } case "GenericType": { return isSameGenericType(first, second as GenericType, topLevel); } case "FunctionType": { return isSameFunctionType(first, second as FunctionType, topLevel); } } } function inferValue( value: Value, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Type { if (parseInt(value.body, 10)) { return FixedType("number", [ ]); } if (value.body === "true" || value.body === "false") { return FixedType("boolean", [ ]); } if (value.body === "toString") { if (valuesInScope[`_${value.body}`]) { return valuesInScope[`_${value.body}`]; } } else { if (valuesInScope[value.body]) { return valuesInScope[value.body]; } } return FixedType("any", [ ]); } function inferStringValue(value: StringValue): Type { return FixedType("string", [ ]); } function inferFormatStringValue(value: FormatStringValue): Type { return FixedType("string", [ ]); } function reduceTypes(types: Type[]): Type[] { return types.reduce((uniques: Type[], type) => { if ( uniques.filter((unique) => isSameType(unique, type, false)) .length === 0 ) { uniques.push(type); } return uniques; }, [ ]); } function inferListValue( value: ListValue, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { if (value.items.length === 0) return Ok(FixedType("List", [ FixedType("any", [ ]) ])); let types: Type[] = [ ]; let actualExpectedType: Type = FixedType("_Inferred", [ ]); if ( expectedType.kind === "FixedType" && expectedType.name === "List" && expectedType.args.length > 0 ) { actualExpectedType = expectedType.args[0]; } for (const item of value.items) { const inferred = inferType( item, actualExpectedType, typedBlocks, imports, valuesInScope ); if (inferred.kind === "Err") return inferred; types.push(inferred.value); } const uniqueTypes = reduceTypes(types); return Ok(FixedType("List", uniqueTypes)); } function inferListRange(value: ListRange): Type { return FixedType("List", [ FixedType("number", [ ]) ]); } function objectLiteralTypeAlias( value: ObjectLiteral, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): TypeAlias { const expectedTypeAlias = getTypeAlias(expectedType, typedBlocks); const typeAlias = TypeAlias( FixedType("Inferred", [ ]), value.fields.map((field) => { const listOfExpected = expectedTypeAlias.kind === "Ok" ? expectedTypeAlias.value.properties.filter( (prop) => prop.name === field.name ) : [ ]; const expected = listOfExpected.length === 0 ? FixedType("_Inferred", [ ]) : listOfExpected[0].type; const inferred = inferType( field.value, expected, typedBlocks, imports, valuesInScope ); if (inferred.kind === "Err") { return Property(field.name, GenericType("any")); } return Property(field.name, inferred.value); }) ); return typeAlias; } function objectLiteralType(typeAlias: TypeAlias): ObjectLiteralType { const fields: Record<string, Type> = {}; for (const prop of typeAlias.properties) { fields[prop.name] = prop.type; } return ObjectLiteralType(fields); } function typeAliasFromObjectLiteralType( objectLiteral: ObjectLiteralType ): TypeAlias { const fields: Property[] = [ ]; for (const name of Object.keys(objectLiteral.properties)) { const type_ = objectLiteral.properties[name]; fields.push(Property(name, type_)); } return TypeAlias(FixedType("Inferred", [ ]), fields); } function inferObjectLiteral( value: ObjectLiteral, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { if (value.base !== null) { return Ok(FixedType("any", [ ])); } const typeAlias = objectLiteralTypeAlias( value, expectedType, typedBlocks, imports, valuesInScope ); if ( expectedType.kind !== "FixedType" || expectedType.name === "_Inferred" ) { return Ok(objectLiteralType(typeAlias)); } for (const block of typedBlocks) { if ( block.kind != "TypeAlias" || block.properties.length !== typeAlias.properties.length || expectedType.name !== block.type.name ) { continue; } let blockMatches = true; for (const inferredProperty of typeAlias.properties) { const hasMatchingBlockProperty = block.properties.filter((prop) => { return ( prop.name === inferredProperty.name && isSameType(prop.type, inferredProperty.type, false) ); }).length > 0; if (!hasMatchingBlockProperty) { blockMatches = false; break; } } if (blockMatches) { return Ok(block.type); } } return Ok(objectLiteralType(typeAlias)); } function inferIfStatement( value: IfStatement, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const ifBranch = inferType( value.ifBody, expectedType, typedBlocks, imports, valuesInScope ); const elseBranch = inferType( value.elseBody, expectedType, typedBlocks, imports, valuesInScope ); if (ifBranch.kind === "Err") return ifBranch; if (elseBranch.kind === "Err") return elseBranch; if (isSameType(ifBranch.value, elseBranch.value, false)) return Ok(ifBranch.value); return Err( `Conflicting types: ${typeToString(ifBranch.value)}, ${typeToString( elseBranch.value )}` ); } function inferBranch( value: Branch, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { return inferType( value.body, expectedType, typedBlocks, imports, valuesInScope ); } function inferCaseStatement( value: CaseStatement, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const typesToReduce = [ ]; for (const branch of value.branches) { const inf = inferBranch( branch, expectedType, typedBlocks, imports, valuesInScope ); if (inf.kind === "Err") return inf; typesToReduce.push(inf.value); } const branches = reduceTypes(typesToReduce); if (branches.length === 1) return Ok(branches[0]); return Err(`Conflicting types: ${branches.map(typeToString).join(", ")}`); } function inferAddition( value: Addition, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const left = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); const right = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) { let maybeStringErrorMessage = ""; if ( value.left.kind === "StringValue" || value.left.kind === "FormatStringValue" ) { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`${ value.left.body }\${${generateExpression(value.right)}}\``; } else if ( value.right.kind === "StringValue" || value.right.kind === "FormatStringValue" ) { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${generateExpression( value.left )}}${value.right.body}\``; } else if ( left.value.kind === "FixedType" && left.value.name === "string" ) { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${generateExpression( value.left )}}\${${generateExpression(value.right)}}\``; } else if ( right.value.kind === "FixedType" && right.value.name === "string" ) { maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${generateExpression( value.left )}}\${${generateExpression(value.right)}}\``; } return Err( `Mismatching types between the left of the addition: ${typeToString( left.value )} and the right of the addition: ${typeToString( right.value )}\nIn Derw, types of both sides of an addition must be the same.${maybeStringErrorMessage}` ); } return left; } function inferSubtraction( value: Subtraction, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const left = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); const right = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return Err( `Mismatching types between ${typeToString( left.value )} and ${typeToString(right.value)}` ); return left; } function inferMultiplication( value: Multiplication, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const left = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); const right = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return Err( `Mismatching types between ${typeToString( left.value )} and ${typeToString(right.value)}` ); return left; } function inferDivision( value: Division, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const left = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); const right = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return Err( `Mismatching types between ${typeToString( left.value )} and ${typeToString(right.value)}` ); return left; } function inferMod( value: Mod, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const left = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); const right = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); if (left.kind === "Err") return left; if (right.kind === "Err") return right; if (!isSameType(left.value, right.value, false)) return Err( `Mismatching types between ${typeToString( left.value )} and ${typeToString(right.value)}` ); return left; } function inferLeftPipe( value: LeftPipe, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const right = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); return right; } function inferRightPipe( value: RightPipe, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const left = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); return left; } function getTypeAliasAtPath( value: ModuleReference, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, TypeAlias> { let currentType = valuesInScope[value.path[0]]; if (!currentType) return Err(""); let currentTypeAlias = getTypeAlias(currentType, typedBlocks); for (const path of value.path.slice(1)) { if (currentTypeAlias.kind === "Err") return Err(""); let found = false; for (const prop of currentTypeAlias.value.properties) { if (prop.name === path) { currentType = prop.type; found = true; break; } } if (!found) { return Err(""); } currentTypeAlias = getTypeAlias(currentType, typedBlocks); } if (currentTypeAlias.kind === "Err") return Err(""); return Ok(currentTypeAlias.value); } function inferModuleReference( value: ModuleReference, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { if (value.path.length > 0) { const isAVariablePath = value.path[0][0].toLowerCase() === value.path[0][0]; if (isAVariablePath && value.value.kind === "Value") { const typeAlias = getTypeAliasAtPath( value, expectedType, typedBlocks, imports, valuesInScope ); if (typeAlias.kind === "Ok") { for (const prop of typeAlias.value.properties) { if (prop.name === value.value.body) { return Ok(prop.type); } } } } } return Ok(FixedType("any", [ ])); } function inferFunctionCall(value: FunctionCall): Type { return FixedType("any", [ ]); } function inferLambda(value: Lambda): Type { return FixedType("any", [ ]); } function inferLambdaCall(value: LambdaCall): Type { return FixedType("any", [ ]); } function tagNames(typedBlocks: TypedBlock[]): string[] { const names = [ ]; for (const block of typedBlocks) { switch (block.kind) { case "TypeAlias": { break; } case "UnionType": { for (const tag of block.tags) { names.push(tag.name); } break; } case "UnionUntaggedType": { break; } } } return names; } function replaceGenerics( type_: Type, replacements: Record<string, Type> ): Type { if ( type_.kind === "FunctionType" || type_.kind === "ObjectLiteralType" || type_.kind === "GenericType" ) { return type_; } return { ...type_, args: type_.args.map((arg: Type): Type => { if (arg.kind === "GenericType" && arg.name in replacements) { return replacements[arg.name]; } else { return arg; } }), }; } function inferConstructor( value: Constructor, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { let seenNameInOtherBlock = false; for (const block of typedBlocks) { if (block.kind === "UnionType") { for (const tag of block.tags) { if (value.constructor === tag.name) { const valid = validateConstructor( value.pattern, expectedType, tag, block, typedBlocks, imports, valuesInScope ); const inferredGenericTypes: Record<string, Type> = {}; for (const arg of tag.args) { if (arg.type.kind === "GenericType") { for (const field of value.pattern.fields) { if (arg.name === field.name) { const fieldIsValid = inferType( field.value, arg.type, typedBlocks, imports, valuesInScope ); if (fieldIsValid.kind === "Ok") { if ( inferredGenericTypes[ arg.type.name ] !== fieldIsValid.value ) { inferredGenericTypes[ arg.type.name ] = fieldIsValid.value; } } } } } } if (valid.kind === "Err") return valid; return Ok( replaceGenerics(block.type, inferredGenericTypes) ); } } } else if (block.kind === "TypeAlias") { if (value.constructor === block.type.name) { seenNameInOtherBlock = true; } } } if (isImportedConstructor(value, imports)) { return Ok(GenericType("any")); } const suggestions = suggestName(value.constructor, tagNames(typedBlocks)); const suggestionsErrorMessage = suggestions.length === 0 ? "" : `\nPerhaps you meant one of these? ${suggestions.join(", ")}`; const hasBeenSeenErrorMesssage = seenNameInOtherBlock ? `\n${value.constructor} refers to a type alias, not a union type constructor.` : ""; return Err( `Did not find constructor ${value.constructor} in scope.${hasBeenSeenErrorMesssage}${suggestionsErrorMessage}` ); } function inferEquality(value: Equality): Type { return FixedType("boolean", [ ]); } function inferInEquality(value: InEquality): Type { return FixedType("boolean", [ ]); } function inferLessThan(value: LessThan): Type { return FixedType("boolean", [ ]); } function inferLessThanOrEqual(value: LessThanOrEqual): Type { return FixedType("boolean", [ ]); } function inferGreaterThan(value: GreaterThan): Type { return FixedType("boolean", [ ]); } function inferGreaterThanOrEqual(value: GreaterThanOrEqual): Type { return FixedType("boolean", [ ]); } function inferAnd(value: And): Type { return FixedType("boolean", [ ]); } function inferOr(value: Or): Type { return FixedType("boolean", [ ]); } function inferListPrepend( value: ListPrepend, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { const leftInfer = inferType( value.left, expectedType, typedBlocks, imports, valuesInScope ); const rightInfer = inferType( value.right, expectedType, typedBlocks, imports, valuesInScope ); if (leftInfer.kind === "Err") { if (value.left.kind === "ObjectLiteral") { const err = validateObjectLiteral( value.left, FixedType("_Inferred", [ ]), typedBlocks, imports, valuesInScope ); if (err.kind === "Err") return err; } return leftInfer; } if (rightInfer.kind === "Err") return rightInfer; if ( rightInfer.value.kind === "GenericType" || (rightInfer.value.kind === "FixedType" && rightInfer.value.name === "any") ) return Ok(FixedType("List", [ GenericType("any") ])); if (rightInfer.value.kind === "FunctionType") { return Err( "Inferred list on right hand side of :: to be a function, not a list" ); } if (rightInfer.value.kind === "ObjectLiteralType") { return Err( "Inferred list on right hand side of :: to be an object literal, not a list" ); } if (rightInfer.value.name === "List" && rightInfer.value.args.length > 0) { const isEmptyList = value.right.kind === "ListValue" && value.right.items.length === 0; if (isEmptyList) { return Ok(FixedType("List", [ leftInfer.value ])); } const listElementType = rightInfer.value.args[0]; if (isSameType(leftInfer.value, listElementType, false)) { return Ok(rightInfer.value); } return Err( `Invalid types in :: - lefthand (${typeToString( leftInfer.value )}) must match elements of righthand (${typeToString( listElementType )})` ); } return Err( `Expected list on righthand side of :: but got ${typeToString( rightInfer.value )}.` ); } export function inferType( expression: Expression, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[], valuesInScope: ScopedValues ): Result<string, Type> { switch (expression.kind) { case "Value": return Ok( inferValue( expression, expectedType, typedBlocks, imports, valuesInScope ) ); case "StringValue": return Ok(inferStringValue(expression)); case "FormatStringValue": return Ok(inferFormatStringValue(expression)); case "ListValue": return inferListValue( expression, expectedType, typedBlocks, imports, valuesInScope ); case "ListRange": return Ok(inferListRange(expression)); case "ObjectLiteral": return inferObjectLiteral( expression, expectedType, typedBlocks, imports, valuesInScope ); case "IfStatement": return inferIfStatement( expression, expectedType, typedBlocks, imports, valuesInScope ); case "CaseStatement": return inferCaseStatement( expression, expectedType, typedBlocks, imports, valuesInScope ); case "Addition": return inferAddition( expression, expectedType, typedBlocks, imports, valuesInScope ); case "Subtraction": return inferSubtraction( expression, expectedType, typedBlocks, imports, valuesInScope ); case "Multiplication": return inferMultiplication( expression, expectedType, typedBlocks, imports, valuesInScope ); case "Division": return inferDivision( expression, expectedType, typedBlocks, imports, valuesInScope ); case "Mod": return inferMod( expression, expectedType, typedBlocks, imports, valuesInScope ); case "And": return Ok(inferAnd(expression)); case "Or": return Ok(inferOr(expression)); case "ListPrepend": return inferListPrepend( expression, expectedType, typedBlocks, imports, valuesInScope ); case "LeftPipe": return inferLeftPipe( expression, expectedType, typedBlocks, imports, valuesInScope ); case "RightPipe": return inferRightPipe( expression, expectedType, typedBlocks, imports, valuesInScope ); case "ModuleReference": return inferModuleReference( expression, expectedType, typedBlocks, imports, valuesInScope ); case "FunctionCall": return Ok(inferFunctionCall(expression)); case "Lambda": return Ok(inferLambda(expression)); case "LambdaCall": return Ok(inferLambdaCall(expression)); case "Constructor": return inferConstructor( expression, expectedType, typedBlocks, imports, valuesInScope ); case "Equality": return Ok(inferEquality(expression)); case "InEquality": return Ok(inferInEquality(expression)); case "LessThan": return Ok(inferLessThan(expression)); case "LessThanOrEqual": return Ok(inferLessThanOrEqual(expression)); case "GreaterThan": return Ok(inferGreaterThan(expression)); case "GreaterThanOrEqual": return Ok(inferGreaterThanOrEqual(expression)); } } function typeToString(type: Type): string { switch (type.kind) { case "GenericType": { return type.name; } case "FixedType": { const typeArgs = type.args.length === 0 ? "" : " (" + type.args.map(typeToString).join(" ") + ")"; return `${type.name}${typeArgs}`.trim(); } case "FunctionType": { return type.args.map(typeToString).join("->"); } case "ObjectLiteralType": { const out = [ ]; for (const name of Object.keys(type.properties)) { out.push(`${name}: ${typeToString(type.properties[name])}`); } return "{ " + out.join(", ") + " }"; } } } function typeExistsInNamespace( type: Type, blocks: TypedBlock[], imports: Import[] ): boolean { if (type.kind === "FunctionType") return true; if (type.kind === "ObjectLiteralType") return true; if (isBuiltinType(type.name)) return true; if (type.name === "List") return true; if (type.kind === "GenericType") return true; for (const block of blocks) { if (isSameType(type, block.type, true)) return true; switch (block.kind) { case "UnionType": { for (const tag of block.tags) { if (isSameType(type, tagToFixedType(tag), true)) return true; } } } } for (const import_ of imports) { for (const module of import_.modules) { for (const exposed of module.exposing) { if (type.name === exposed) return true; } if ( type.name.indexOf(".") > -1 && module.alias.kind === "Just" && type.name.split(".")[0] === module.alias.value ) { return true; } } } return false; } function finalExpressions(expression: Expression): string[] { switch (expression.kind) { case "Value": return [ ]; case "StringValue": return [ expression.body ]; case "FormatStringValue": return [ ]; case "ListValue": return [ ]; case "ListRange": return [ ]; case "ObjectLiteral": return [ ]; case "IfStatement": return finalExpressions(expression.ifBody).concat( finalExpressions(expression.elseBody) ); case "CaseStatement": let expressions: string[] = [ ]; for (const branch of expression.branches) { expressions = expressions.concat(finalExpressions(branch.body)); } return expressions; case "Addition": return [ ]; case "Subtraction": return [ ]; case "Multiplication": return [ ]; case "Division": return [ ]; case "Mod": return [ ]; case "And": return [ ]; case "Or": return [ ]; case "ListPrepend": return [ ]; case "LeftPipe": return [ ]; case "RightPipe": return [ ]; case "ModuleReference": return [ ]; case "FunctionCall": return [ ]; case "Lambda": return [ ]; case "LambdaCall": return [ ]; case "Constructor": return [ ]; case "Equality": return [ ]; case "InEquality": return [ ]; case "LessThan": return [ ]; case "LessThanOrEqual": return [ ]; case "GreaterThan": return [ ]; case "GreaterThanOrEqual": return [ ]; } } function allFinalExpressions(block: Block): string[] { switch (block.kind) { case "Const": { return finalExpressions(block.value); } case "Function": { return finalExpressions(block.body); } default: { return [ ]; } } } function validateAllBranchesCovered( typedBlocks: TypedBlock[], containingBlock: Const | Function, expression: CaseStatement ): Result<string, true> { const hasDefault = expression.branches.filter((b) => b.pattern.kind === "Default").length > 0; const casePattern = expression.predicate; let predicateType: Type | null = null; if (casePattern.kind === "Value") { if (containingBlock.kind === "Function") { for (const arg of containingBlock.args) { if (arg.kind === "FunctionArg") { if (arg.name === casePattern.body) { predicateType = arg.type; } } } } } if (predicateType && predicateType?.kind === "FixedType") { const matchingBlocks = typedBlocks.filter((b) => isSameType(b.type, predicateType as Type, false) ); if (matchingBlocks.length > 0) { const matchingBlock = matchingBlocks[0]; if (matchingBlock.kind === "UnionUntaggedType") { const strings = matchingBlock.values.map((s) => s.body); const seenStrings: string[] = [ ]; for (const branch of expression.branches) { if (branch.pattern.kind === "StringValue") { seenStrings.push(branch.pattern.body); } } const missingBranches = strings.filter( (s) => seenStrings.indexOf(s) === -1 ); const extraBranches = seenStrings.filter( (s) => strings.indexOf(s) === -1 ); let errors = [ ]; if (missingBranches.length > 0 && !hasDefault) { errors.push( `All possible branches of a untagged union must be covered. I expected a branch for ${missingBranches .map((s) => `"${s}"`) .join( " | " )} but they were missing. If you don't need one, have a default branch` ); } if (extraBranches.length > 0) { errors.push( `I got too many branches. The branches for ${extraBranches .map((s) => `"${s}"`) .join( " | " )} aren't part of the untagged union so will never be true. Remove them.` ); } if (errors.length > 0) { errors = [ `The case statement did not match the untagged union ${typeToString( predicateType )}`, ...errors, ]; return Err(errors.join("\n")); } return Ok(true); } else if (matchingBlock.kind === "UnionType") { const names = matchingBlock.tags.map((t) => t.name); const seenNames: string[] = [ ]; for (const branch of expression.branches) { if (branch.pattern.kind === "Destructure") { seenNames.push(branch.pattern.constructor); } } const missingBranches = names.filter( (s) => seenNames.indexOf(s) === -1 ); const extraBranches = seenNames.filter( (s) => names.indexOf(s) === -1 ); let errors = [ ]; if (missingBranches.length > 0 && !hasDefault) { errors.push( `All possible branches of a union must be covered. I expected a branch for ${missingBranches.join( " | " )} but they were missing. If you don't need one, have a default branch` ); } if (extraBranches.length > 0) { errors.push( `I got too many branches. The branches for ${extraBranches.join( " | " )} aren't part of the union so will never be true. Remove them.` ); } if (errors.length > 0) { errors = [ `The case statement did not match the union ${typeToString( predicateType )}`, ...errors, ]; return Err(errors.join("\n")); } return Ok(true); } } } return Ok(true); } export function getCasesFromFunction(block: Function): CaseStatement[] { const body = block.body; const statements = [ ]; if (body.kind === "CaseStatement") statements.push(body); return statements; } export function validateAllCasesCovered( block: Block, typedBlocks: TypedBlock[] ): string[] { if (block.kind !== "Function") { return [ ]; } const cases = getCasesFromFunction(block); const invalidBranches: string[] = [ ]; for (const case_ of cases) { const valid = validateAllBranchesCovered(typedBlocks, block, case_); if (valid.kind === "Err") { invalidBranches.push(valid.error); } } return invalidBranches; } export function validateObjectLiteralType( objectLiteralType: ObjectLiteralType, expectedType: Type, typedBlocks: TypedBlock[], imports: Import[] ): Result<string, null> { const typeAlias = typeAliasFromObjectLiteralType(objectLiteralType); if (expectedType.kind !== "FixedType") return Ok(null); for (const typeBlock of typedBlocks) { if ( typeBlock.kind === "UnionType" || typeBlock.kind === "UnionUntaggedType" || typeBlock.type.name !== expectedType.name ) continue; const missingPropertyFromTypeAlias: Property[] = [ ]; const addedProperties: Property[] = [ ]; const incorrectProperties: string[] = [ ]; for (const property of typeAlias.properties) { let found = false; for (const typeProperty of typeBlock.properties) { if (property.name === typeProperty.name) { if ( isImportedType(typeProperty.type, imports) || isImportedType(property.type, imports) ) { found = true; continue; } else if ( !isSameType(property.type, typeProperty.type, false) ) { incorrectProperties.push( `${property.name}: Expected ${typeToString( typeProperty.type )} but got ${typeToString(property.type)}` ); } found = true; break; } } if (!found) { addedProperties.push(property); } } for (const typeProperty of typeBlock.properties) { let found = false; for (const property of typeAlias.properties) { if (property.name === typeProperty.name) { found = true; break; } } if (!found) { missingPropertyFromTypeAlias.push(typeProperty); } } if ( missingPropertyFromTypeAlias.length > 0 || addedProperties.length > 0 || incorrectProperties.length > 0 ) { let errorMessage = ""; if (missingPropertyFromTypeAlias.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `The type alias had these properties which are missing in this object literal: ${missingPropertyFromTypeAlias .map((prop) => `${prop.name}: ${typeToString(prop.type)}`) .join(" | ")}`; } if (addedProperties.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `The object literal had these properties which aren't in the type alias: ${addedProperties .map((prop) => `${prop.name}: ${typeToString(prop.type)}`) .join(" | ")}`; } if (incorrectProperties.length > 0) { if (errorMessage.length > 0) errorMessage += "\n"; errorMessage += `Invalid properties: ${incorrectProperties.join( " | " )}`; } return Err( `Object literal type alias ${typeToString( typeBlock.type )} did not match the value due to:\n${errorMessage}` ); } } return Ok(null); } function getUntaggedUnion( type_: Type, typedBlocks: TypedBlock[] ): Result<null, UnionUntaggedType>