derw
Version:
An Elm-inspired language that transpiles to TypeScript
1,183 lines • 68.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateType = exports.getValuesInTopLevelScope = exports.findReplacement = exports.validateObjectLiteralType = exports.validateAllCasesCovered = exports.getCasesFromFunction = exports.inferType = exports.isSameType = void 0;
const result_1 = require("@eeue56/ts-core/build/main/lib/result");
const builtins_1 = require("./builtins");
const distance_1 = require("./errors/distance");
const Derw_1 = require("./generators/Derw");
const types_1 = require("./types");
function isSameGenericType(first, second, topLevel) {
if (topLevel)
return true;
// todo: figure this out
//return first.name === second.name;
return true;
}
function isSameFixedType(first, second, topLevel) {
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, second, topLevel) {
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, second, topLevel) {
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, second) {
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, expectedType, typedBlocks) {
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) {
return (0, types_1.FixedType)(tag.name, tag.args
.filter((tag) => tag.type.kind === "GenericType")
.map((arg) => arg.type));
}
function isATag(type_, typedBlocks) {
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 (0, result_1.Ok)(block.type);
}
}
}
}
return (0, result_1.Err)(null);
}
function isSameType(first, second, topLevel, typedBlocks = []) {
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 = Object.assign(Object.assign({}, first), { name: split[split.length - 1] });
}
second = second;
if (second.name.indexOf(".") > -1) {
const split = second.name.split(".");
second = Object.assign(Object.assign({}, 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, topLevel);
}
case "FunctionType": {
return isSameFunctionType(first, second, topLevel);
}
}
}
exports.isSameType = isSameType;
function inferValue(value, expectedType, typedBlocks, imports, valuesInScope) {
if (parseInt(value.body, 10)) {
return (0, types_1.FixedType)("number", []);
}
if (value.body === "true" || value.body === "false") {
return (0, types_1.FixedType)("boolean", []);
}
if (value.body === "toString") {
if (valuesInScope[`_${value.body}`]) {
return valuesInScope[`_${value.body}`];
}
}
else {
if (valuesInScope[value.body]) {
return valuesInScope[value.body];
}
}
return (0, types_1.FixedType)("any", []);
}
function inferStringValue(value) {
return (0, types_1.FixedType)("string", []);
}
function inferFormatStringValue(value) {
return (0, types_1.FixedType)("string", []);
}
function reduceTypes(types) {
return types.reduce((uniques, type) => {
if (uniques.filter((unique) => isSameType(unique, type, false))
.length === 0) {
uniques.push(type);
}
return uniques;
}, []);
}
function inferListValue(value, expectedType, typedBlocks, imports, valuesInScope) {
if (value.items.length === 0)
return (0, result_1.Ok)((0, types_1.FixedType)("List", [(0, types_1.FixedType)("any", [])]));
let types = [];
let actualExpectedType = (0, types_1.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 (0, result_1.Ok)((0, types_1.FixedType)("List", uniqueTypes));
}
function inferListRange(value) {
return (0, types_1.FixedType)("List", [(0, types_1.FixedType)("number", [])]);
}
function objectLiteralTypeAlias(value, expectedType, typedBlocks, imports, valuesInScope) {
const expectedTypeAlias = getTypeAlias(expectedType, typedBlocks);
const typeAlias = (0, types_1.TypeAlias)((0, types_1.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
? (0, types_1.FixedType)("_Inferred", [])
: listOfExpected[0].type;
const inferred = inferType(field.value, expected, typedBlocks, imports, valuesInScope);
if (inferred.kind === "Err") {
return (0, types_1.Property)(field.name, (0, types_1.GenericType)("any"));
}
return (0, types_1.Property)(field.name, inferred.value);
}));
return typeAlias;
}
function objectLiteralType(typeAlias) {
const fields = {};
for (const prop of typeAlias.properties) {
fields[prop.name] = prop.type;
}
return (0, types_1.ObjectLiteralType)(fields);
}
function typeAliasFromObjectLiteralType(objectLiteral) {
const fields = [];
for (const name of Object.keys(objectLiteral.properties)) {
const type_ = objectLiteral.properties[name];
fields.push((0, types_1.Property)(name, type_));
}
return (0, types_1.TypeAlias)((0, types_1.FixedType)("Inferred", []), fields);
}
function inferObjectLiteral(value, expectedType, typedBlocks, imports, valuesInScope) {
if (value.base !== null) {
return (0, result_1.Ok)((0, types_1.FixedType)("any", []));
}
const typeAlias = objectLiteralTypeAlias(value, expectedType, typedBlocks, imports, valuesInScope);
if (expectedType.kind !== "FixedType" ||
expectedType.name === "_Inferred") {
return (0, result_1.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 (0, result_1.Ok)(block.type);
}
}
return (0, result_1.Ok)(objectLiteralType(typeAlias));
}
function inferIfStatement(value, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Ok)(ifBranch.value);
return (0, result_1.Err)(`Conflicting types: ${typeToString(ifBranch.value)}, ${typeToString(elseBranch.value)}`);
}
function inferBranch(value, expectedType, typedBlocks, imports, valuesInScope) {
return inferType(value.body, expectedType, typedBlocks, imports, valuesInScope);
}
function inferCaseStatement(value, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Ok)(branches[0]);
return (0, result_1.Err)(`Conflicting types: ${branches.map(typeToString).join(", ")}`);
}
function inferAddition(value, expectedType, typedBlocks, imports, valuesInScope) {
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}\${${(0, Derw_1.generateExpression)(value.right)}}\``;
}
else if (value.right.kind === "StringValue" ||
value.right.kind === "FormatStringValue") {
maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${(0, Derw_1.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, \`\${${(0, Derw_1.generateExpression)(value.left)}}\${${(0, Derw_1.generateExpression)(value.right)}}\``;
}
else if (right.value.kind === "FixedType" &&
right.value.name === "string") {
maybeStringErrorMessage = `\nTry using a format string via \`\` instead\nFor example, \`\${${(0, Derw_1.generateExpression)(value.left)}}\${${(0, Derw_1.generateExpression)(value.right)}}\``;
}
return (0, result_1.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, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`);
return left;
}
function inferMultiplication(value, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`);
return left;
}
function inferDivision(value, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`);
return left;
}
function inferMod(value, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Err)(`Mismatching types between ${typeToString(left.value)} and ${typeToString(right.value)}`);
return left;
}
function inferLeftPipe(value, expectedType, typedBlocks, imports, valuesInScope) {
const right = inferType(value.right, expectedType, typedBlocks, imports, valuesInScope);
return right;
}
function inferRightPipe(value, expectedType, typedBlocks, imports, valuesInScope) {
const left = inferType(value.left, expectedType, typedBlocks, imports, valuesInScope);
return left;
}
function getTypeAliasAtPath(value, expectedType, typedBlocks, imports, valuesInScope) {
let currentType = valuesInScope[value.path[0]];
if (!currentType)
return (0, result_1.Err)("");
let currentTypeAlias = getTypeAlias(currentType, typedBlocks);
for (const path of value.path.slice(1)) {
if (currentTypeAlias.kind === "Err")
return (0, result_1.Err)("");
let found = false;
for (const prop of currentTypeAlias.value.properties) {
if (prop.name === path) {
currentType = prop.type;
found = true;
break;
}
}
if (!found) {
return (0, result_1.Err)("");
}
currentTypeAlias = getTypeAlias(currentType, typedBlocks);
}
if (currentTypeAlias.kind === "Err")
return (0, result_1.Err)("");
return (0, result_1.Ok)(currentTypeAlias.value);
}
function inferModuleReference(value, expectedType, typedBlocks, imports, valuesInScope) {
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 (0, result_1.Ok)(prop.type);
}
}
}
}
}
return (0, result_1.Ok)((0, types_1.FixedType)("any", []));
}
function inferFunctionCall(value) {
return (0, types_1.FixedType)("any", []);
}
function inferLambda(value) {
return (0, types_1.FixedType)("any", []);
}
function inferLambdaCall(value) {
return (0, types_1.FixedType)("any", []);
}
function tagNames(typedBlocks) {
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_, replacements) {
if (type_.kind === "FunctionType" ||
type_.kind === "ObjectLiteralType" ||
type_.kind === "GenericType") {
return type_;
}
return Object.assign(Object.assign({}, type_), { args: type_.args.map((arg) => {
if (arg.kind === "GenericType" && arg.name in replacements) {
return replacements[arg.name];
}
else {
return arg;
}
}) });
}
function inferConstructor(value, expectedType, typedBlocks, imports, valuesInScope) {
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 = {};
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 (0, result_1.Ok)(replaceGenerics(block.type, inferredGenericTypes));
}
}
}
else if (block.kind === "TypeAlias") {
if (value.constructor === block.type.name) {
seenNameInOtherBlock = true;
}
}
}
if (isImportedConstructor(value, imports)) {
return (0, result_1.Ok)((0, types_1.GenericType)("any"));
}
const suggestions = (0, distance_1.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 (0, result_1.Err)(`Did not find constructor ${value.constructor} in scope.${hasBeenSeenErrorMesssage}${suggestionsErrorMessage}`);
}
function inferEquality(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferInEquality(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferLessThan(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferLessThanOrEqual(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferGreaterThan(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferGreaterThanOrEqual(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferAnd(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferOr(value) {
return (0, types_1.FixedType)("boolean", []);
}
function inferListPrepend(value, expectedType, typedBlocks, imports, valuesInScope) {
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, (0, types_1.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 (0, result_1.Ok)((0, types_1.FixedType)("List", [(0, types_1.GenericType)("any")]));
if (rightInfer.value.kind === "FunctionType") {
return (0, result_1.Err)("Inferred list on right hand side of :: to be a function, not a list");
}
if (rightInfer.value.kind === "ObjectLiteralType") {
return (0, result_1.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 (0, result_1.Ok)((0, types_1.FixedType)("List", [leftInfer.value]));
}
const listElementType = rightInfer.value.args[0];
if (isSameType(leftInfer.value, listElementType, false)) {
return (0, result_1.Ok)(rightInfer.value);
}
return (0, result_1.Err)(`Invalid types in :: - lefthand (${typeToString(leftInfer.value)}) must match elements of righthand (${typeToString(listElementType)})`);
}
return (0, result_1.Err)(`Expected list on righthand side of :: but got ${typeToString(rightInfer.value)}.`);
}
function inferType(expression, expectedType, typedBlocks, imports, valuesInScope) {
switch (expression.kind) {
case "Value":
return (0, result_1.Ok)(inferValue(expression, expectedType, typedBlocks, imports, valuesInScope));
case "StringValue":
return (0, result_1.Ok)(inferStringValue(expression));
case "FormatStringValue":
return (0, result_1.Ok)(inferFormatStringValue(expression));
case "ListValue":
return inferListValue(expression, expectedType, typedBlocks, imports, valuesInScope);
case "ListRange":
return (0, result_1.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 (0, result_1.Ok)(inferAnd(expression));
case "Or":
return (0, result_1.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 (0, result_1.Ok)(inferFunctionCall(expression));
case "Lambda":
return (0, result_1.Ok)(inferLambda(expression));
case "LambdaCall":
return (0, result_1.Ok)(inferLambdaCall(expression));
case "Constructor":
return inferConstructor(expression, expectedType, typedBlocks, imports, valuesInScope);
case "Equality":
return (0, result_1.Ok)(inferEquality(expression));
case "InEquality":
return (0, result_1.Ok)(inferInEquality(expression));
case "LessThan":
return (0, result_1.Ok)(inferLessThan(expression));
case "LessThanOrEqual":
return (0, result_1.Ok)(inferLessThanOrEqual(expression));
case "GreaterThan":
return (0, result_1.Ok)(inferGreaterThan(expression));
case "GreaterThanOrEqual":
return (0, result_1.Ok)(inferGreaterThanOrEqual(expression));
}
}
exports.inferType = inferType;
function typeToString(type) {
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, blocks, imports) {
if (type.kind === "FunctionType")
return true;
if (type.kind === "ObjectLiteralType")
return true;
if ((0, builtins_1.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) {
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 = [];
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) {
switch (block.kind) {
case "Const": {
return finalExpressions(block.value);
}
case "Function": {
return finalExpressions(block.body);
}
default: {
return [];
}
}
}
function validateAllBranchesCovered(typedBlocks, containingBlock, expression) {
const hasDefault = expression.branches.filter((b) => b.pattern.kind === "Default").length >
0;
const casePattern = expression.predicate;
let predicateType = 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 === null || predicateType === void 0 ? void 0 : predicateType.kind) === "FixedType") {
const matchingBlocks = typedBlocks.filter((b) => isSameType(b.type, predicateType, false));
if (matchingBlocks.length > 0) {
const matchingBlock = matchingBlocks[0];
if (matchingBlock.kind === "UnionUntaggedType") {
const strings = matchingBlock.values.map((s) => s.body);
const seenStrings = [];
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 (0, result_1.Err)(errors.join("\n"));
}
return (0, result_1.Ok)(true);
}
else if (matchingBlock.kind === "UnionType") {
const names = matchingBlock.tags.map((t) => t.name);
const seenNames = [];
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 (0, result_1.Err)(errors.join("\n"));
}
return (0, result_1.Ok)(true);
}
}
}
return (0, result_1.Ok)(true);
}
function getCasesFromFunction(block) {
const body = block.body;
const statements = [];
if (body.kind === "CaseStatement")
statements.push(body);
return statements;
}
exports.getCasesFromFunction = getCasesFromFunction;
function validateAllCasesCovered(block, typedBlocks) {
if (block.kind !== "Function") {
return [];
}
const cases = getCasesFromFunction(block);
const invalidBranches = [];
for (const case_ of cases) {
const valid = validateAllBranchesCovered(typedBlocks, block, case_);
if (valid.kind === "Err") {
invalidBranches.push(valid.error);
}
}
return invalidBranches;
}
exports.validateAllCasesCovered = validateAllCasesCovered;
function validateObjectLiteralType(objectLiteralType, expectedType, typedBlocks, imports) {
const typeAlias = typeAliasFromObjectLiteralType(objectLiteralType);
if (expectedType.kind !== "FixedType")
return (0, result_1.Ok)(null);
for (const typeBlock of typedBlocks) {
if (typeBlock.kind === "UnionType" ||
typeBlock.kind === "UnionUntaggedType" ||
typeBlock.type.name !== expectedType.name)
continue;
const missingPropertyFromTypeAlias = [];
const addedProperties = [];
const incorrectProperties = [];
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 (0, result_1.Err)(`Object literal type alias ${typeToString(typeBlock.type)} did not match the value due to:\n${errorMessage}`);
}
}
return (0, result_1.Ok)(null);
}
exports.validateObjectLiteralType = validateObjectLiteralType;
function getUntaggedUnion(type_, typedBlocks) {
if (type_.kind !== "FixedType")
return (0, result_1.Err)(null);
for (const block of typedBlocks) {
if (block.kind !== "UnionUntaggedType")
continue;
if (block.type.name === type_.name) {
return (0, result_1.Ok)(block);
}
}
return (0, result_1.Err)(null);
}
function getTypeAlias(type_, typedBlocks) {
if (type_.kind !== "FixedType")
return (0, result_1.Err)(null);
for (const block of typedBlocks) {
if (block.kind !== "TypeAlias")
continue;
if (block.type.name === type_.name) {
return (0, result_1.Ok)(block);
}
}
return (0, result_1.Err)(null);
}
function validateObjectLiteral(objectLiteral, expectedType, typedBlocks, imports, valuesInScope) {
if (objectLiteral.base !== null) {
return (0, result_1.Ok)(null);
}
if (expectedType.kind !== "FixedType")
return (0, result_1.Ok)(null);
const typeAlias = objectLiteralTypeAlias(objectLiteral, expectedType, typedBlocks, imports, valuesInScope);
for (const typeBlock of typedBlocks) {
if (typeBlock.kind === "UnionType" ||
typeBlock.kind === "UnionUntaggedType" ||
typeBlock.type.name !== expectedType.name)
continue;
const missingPropertyFromTypeAlias = [];
const addedProperties = [];
const incorrectProperties = [];
for (const property of typeAlias.properties) {
let found = false;
for (const typeProperty of typeBlock.properties) {
if (property.name === typeProperty.name) {
const maybeUntaggedUnionBlock = getUntaggedUnion(typeProperty.type, typedBlocks);
if (maybeUntaggedUnionBlock.kind === "Ok") {
const fieldValue = objectLiteral.fields.filter((field) => field.name === property.name)[0];
if (!fieldValue)
continue;
if (fieldValue.value.kind !== "StringValue")
continue;
const stringFieldValue = fieldValue.value.body;
const isCovered = maybeUntaggedUnionBlock.value.values.filter((v) => v.body === stringFieldValue).length > 0;
if (!isCovered) {
incorrectProperties.push(`${fieldValue.name}: Expected ${typeToString(maybeUntaggedUnionBlock.value.type)}, composed of ${maybeUntaggedUnionBlock.value.values
.map((v) => `"${v.body}"`)
.join(" | ")}\` but got "${stringFieldValue}"`);
}
}
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 (0, result_1.Err)(`Mismatching type for type alias ${typeToString(typeBlock.type)}\n${errorMessage}`);
}
}
return (0, result_1.Ok)(null);
}
function unifyType(type_, typeReplacements) {
switch (type_.kind) {
case "FixedType": {
return Object.assign(Object.assign({}, type_), { args: type_.args.map((arg) => {
return unifyType(arg, typeReplacements);
}) });
}
case "FunctionType": {
return Object.assign(Object.assign({}, type_), { args: type_.args.map((arg) => {
return unifyType(arg, typeReplacements);
}) });
}
case "GenericType": {
return typeReplacements[type_.name] || type_;
}
case "ObjectLiteralType": {
const newProperties = {};
for (const propName of Object.keys(type_.properties)) {
const propValue = type_.properties[propName];
newProperties[propName] = unifyType(propValue, typeReplacements);
}
return Object.assign(Object.assign({}, type_), { properties: newProperties });
}
}
}
function unifyTag(tag, typeReplacements) {
return Object.assign(Object.assign({}, tag), { args: tag.args.map((arg) => {
return Object.assign(Object.assign({}, arg), { type: unifyType(arg.type, typeReplacements) });
}) });
}
function unifyUnionType(unionType, expectedType) {
if (expectedType.kind !== "FixedType")
return unionType;
if (unionType.type.name !== expectedType.name) {
return unionType;
}
const toBeReplaced = {};
for (var i = 0; i < expectedType.args.length; i++) {
const currentGenericArg = unionType.type.args[i];
if (currentGenericArg.kind !== "GenericType") {
continue;
}
const actualFixedArg = expectedType.args[i];
toBeReplaced[currentGenericArg.name] = actualFixedArg;
}
return Object.assign(Object.assign({}, unionType), { tags: unionType.tags.map((tag) => {
return unifyTag(tag, toBeReplaced);
}) });
}
function tagTypeAlias(tag, genericUnionType, expectedType, typedBlocks) {
const unionType = unifyUnionType(genericUnionType, expectedType);
const tagToUse = unionType.tags.filter((localTag) => localTag.name === tag.name)[0] ||
tag;
const typeAlias = (0, types_1.TypeAlias)((0, types_1.FixedType)("Inferred", []), tagToUse.args.map((arg) => {
return (0, types_1.Property)(arg.name, arg.type);
}));
return typeAlias;
}
function findReplacement(inferredType, expectedType, typedBlocks) {
switch (inferredType.kind) {
case "FixedType": {
if (expectedType.kind !== "FixedType" ||
expectedType.name !== inferredType.name ||
expectedType.args.length !== inferredType.args.length) {
return inferredType;
}
const args = [];
for (let i = 0; i < inferredType.args.length; i++) {
const inferredArg = inferredType.args[i];
const expectedArg = expectedType.args[i];
args.push(findReplacement(inferredArg, expectedArg, typedBlocks));
}
return Object.assign(