Repository: elm-tooling/elm-language-server
Pull request #310 (2020-08-22 05:06:10)

Type inference
Core type inference support for most language features besides Records. No need to merge this anytime soon, especially not before next release, but I wanted to get your feedback on what I have so far. 

We need to figure out what to do for diagnostics, because I don't think we can do type inference diagnostics on change and elm analyse on change at the same time. I think it would be good to add a setting to opt in to using type inference for on change diagnostics and other language features. Once it becomes stable enough, we can just turn it on by default. 

I went with the approach to map every `SyntaxNode` to an `Expression`, which gives better type control over the expression types and getting child nodes. This is really nice to work with, but my only concern is speed, which I'm not sure will be an issue or not. We certainly could do everything with just `SyntaxNode`s, but it would be more complex and less reliable. 

If you want to test it, you can uncomment the blocks in `hoverProvider` and `diagnosticsProvider`. Then you can hover over function declarations to see the inferred type and you will have diagnostics on change. The diagnostics are basic right now, but I think we should try to make them similar to the compiler. 

Feel free to contribute on what is there so far, especially around diagnostics and ast updating (Since they overlap with existing code). I will be working on Record support, so it shouldn't interfere. 


Patch:
diff --git a/package-lock.json b/package-lock.json
index 2618055..a2e609b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -453,6 +453,59 @@
         "minimist": "^1.2.0"
       }
     },
+    "@eslint/eslintrc": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
+      "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.12.4",
+        "debug": "^4.1.1",
+        "espree": "^7.3.0",
+        "globals": "^12.1.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^3.13.1",
+        "lodash": "^4.17.19",
+        "minimatch": "^3.0.4",
+        "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.12.5",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz",
+          "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "debug": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ignore": {
+          "version": "4.0.6",
+          "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+          "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
     "@istanbuljs/load-nyc-config": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1061,12 +1114,6 @@
       "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
       "dev": true
     },
-    "@types/eslint-visitor-keys": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
-      "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
-      "dev": true
-    },
     "@types/graceful-fs": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz",
@@ -1102,9 +1149,9 @@
       }
     },
     "@types/jest": {
-      "version": "26.0.10",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.10.tgz",
-      "integrity": "sha512-i2m0oyh8w/Lum7wWK/YOZJakYF8Mx08UaKA1CtbmFeDquVhAEdA7znacsVSf2hJ1OQ/OfVMGN90pw/AtzF8s/Q==",
+      "version": "26.0.14",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.14.tgz",
+      "integrity": "sha512-Hz5q8Vu0D288x3iWXePSn53W7hAjP0H7EQ6QvDO9c7t46mR0lNOLlfuwQ+JkVxuhygHzlzPX+0jKdA3ZgSh+Vg==",
       "dev": true,
       "requires": {
         "jest-diff": "^25.2.1",
@@ -1112,15 +1159,15 @@
       }
     },
     "@types/json-schema": {
-      "version": "7.0.5",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
-      "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
+      "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
       "dev": true
     },
     "@types/node": {
-      "version": "14.6.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.1.tgz",
-      "integrity": "sha512-HnYlg/BRF8uC1FyKRFZwRaCPTPYKa+6I8QiUZFLredaGOou481cgFS4wKRFyKvQtX8xudqkSdBczJHIYSQYKrQ==",
+      "version": "14.11.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",
+      "integrity": "sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==",
       "dev": true
     },
     "@types/normalize-package-data": {
@@ -1157,12 +1204,13 @@
       "dev": true
     },
     "@typescript-eslint/eslint-plugin": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz",
-      "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.2.0.tgz",
+      "integrity": "sha512-zBNRkzvLSwo6y5TG0DVcmshZIYBHKtmzD4N+LYnfTFpzc4bc79o8jNRSb728WV7A4Cegbs+MV5IRAj8BKBgOVQ==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/experimental-utils": "3.10.1",
+        "@typescript-eslint/experimental-utils": "4.2.0",
+        "@typescript-eslint/scope-manager": "4.2.0",
         "debug": "^4.1.1",
         "functional-red-black-tree": "^1.0.1",
         "regexpp": "^3.0.0",
@@ -1171,12 +1219,12 @@
       },
       "dependencies": {
         "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
           "dev": true,
           "requires": {
-            "ms": "^2.1.1"
+            "ms": "2.1.2"
           }
         },
         "ms": {
@@ -1188,47 +1236,74 @@
       }
     },
     "@typescript-eslint/experimental-utils": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz",
-      "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.2.0.tgz",
+      "integrity": "sha512-5BBj6BjgHEndBaQQpUVzRIPERz03LBc0MCQkHwUaH044FJFL08SwWv/sQftk7gf0ShZ2xZysz0LTwCwNt4Xu3w==",
       "dev": true,
       "requires": {
         "@types/json-schema": "^7.0.3",
-        "@typescript-eslint/types": "3.10.1",
-        "@typescript-eslint/typescript-estree": "3.10.1",
+        "@typescript-eslint/scope-manager": "4.2.0",
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/typescript-estree": "4.2.0",
         "eslint-scope": "^5.0.0",
         "eslint-utils": "^2.0.0"
       }
     },
     "@typescript-eslint/parser": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz",
-      "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.2.0.tgz",
+      "integrity": "sha512-54jJ6MwkOtowpE48C0QJF9iTz2/NZxfKVJzv1ha5imigzHbNSLN9yvbxFFH1KdlRPQrlR8qxqyOvLHHxd397VA==",
       "dev": true,
       "requires": {
-        "@types/eslint-visitor-keys": "^1.0.0",
-        "@typescript-eslint/experimental-utils": "3.10.1",
-        "@typescript-eslint/types": "3.10.1",
-        "@typescript-eslint/typescript-estree": "3.10.1",
-        "eslint-visitor-keys": "^1.1.0"
+        "@typescript-eslint/scope-manager": "4.2.0",
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/typescript-estree": "4.2.0",
+        "debug": "^4.1.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+          "dev": true,
+          "requires": {
+            "ms": "2.1.2"
+          }
+        },
+        "ms": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+          "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+          "dev": true
+        }
+      }
+    },
+    "@typescript-eslint/scope-manager": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz",
+      "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==",
+      "dev": true,
+      "requires": {
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/visitor-keys": "4.2.0"
       }
     },
     "@typescript-eslint/types": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz",
-      "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz",
+      "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==",
       "dev": true
     },
     "@typescript-eslint/typescript-estree": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz",
-      "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz",
+      "integrity": "sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==",
       "dev": true,
       "requires": {
-        "@typescript-eslint/types": "3.10.1",
-        "@typescript-eslint/visitor-keys": "3.10.1",
+        "@typescript-eslint/types": "4.2.0",
+        "@typescript-eslint/visitor-keys": "4.2.0",
         "debug": "^4.1.1",
-        "glob": "^7.1.6",
+        "globby": "^11.0.1",
         "is-glob": "^4.0.1",
         "lodash": "^4.17.15",
         "semver": "^7.3.2",
@@ -1236,12 +1311,12 @@
       },
       "dependencies": {
         "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
           "dev": true,
           "requires": {
-            "ms": "^2.1.1"
+            "ms": "2.1.2"
           }
         },
         "ms": {
@@ -1253,12 +1328,13 @@
       }
     },
     "@typescript-eslint/visitor-keys": {
-      "version": "3.10.1",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz",
-      "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==",
       "dev": true,
       "requires": {
-        "eslint-visitor-keys": "^1.1.0"
+        "@typescript-eslint/types": "4.2.0",
+        "eslint-visitor-keys": "^2.0.0"
       }
     },
     "abab": {
@@ -1293,9 +1369,9 @@
       }
     },
     "acorn-jsx": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
-      "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
+      "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
       "dev": true
     },
     "acorn-walk": {
@@ -2515,12 +2591,13 @@
       }
     },
     "eslint": {
-      "version": "7.7.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz",
-      "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.9.0.tgz",
+      "integrity": "sha512-V6QyhX21+uXp4T+3nrNfI3hQNBDa/P8ga7LoQOenwrlEFXrEnUEE+ok1dMtaS3b6rmLXhT1TkTIsG75HMLbknA==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
+        "@eslint/eslintrc": "^0.1.3",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
@@ -2530,7 +2607,7 @@
         "eslint-scope": "^5.1.0",
         "eslint-utils": "^2.1.0",
         "eslint-visitor-keys": "^1.3.0",
-        "espree": "^7.2.0",
+        "espree": "^7.3.0",
         "esquery": "^1.2.0",
         "esutils": "^2.0.2",
         "file-entry-cache": "^5.0.1",
@@ -2569,14 +2646,20 @@
           }
         },
         "debug": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
-          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "version": "4.2.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+          "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
           "dev": true,
           "requires": {
-            "ms": "^2.1.1"
+            "ms": "2.1.2"
           }
         },
+        "eslint-visitor-keys": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+          "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+          "dev": true
+        },
         "ignore": {
           "version": "4.0.6",
           "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -2610,12 +2693,12 @@
       }
     },
     "eslint-scope": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
-      "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
       "dev": true,
       "requires": {
-        "esrecurse": "^4.1.0",
+        "esrecurse": "^4.3.0",
         "estraverse": "^4.1.1"
       }
     },
@@ -2626,23 +2709,39 @@
       "dev": true,
       "requires": {
         "eslint-visitor-keys": "^1.1.0"
+      },
+      "dependencies": {
+        "eslint-visitor-keys": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+          "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+          "dev": true
+        }
       }
     },
     "eslint-visitor-keys": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
-      "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
+      "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
       "dev": true
     },
     "espree": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz",
-      "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==",
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz",
+      "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==",
       "dev": true,
       "requires": {
-        "acorn": "^7.3.1",
+        "acorn": "^7.4.0",
         "acorn-jsx": "^5.2.0",
         "eslint-visitor-keys": "^1.3.0"
+      },
+      "dependencies": {
+        "eslint-visitor-keys": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+          "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+          "dev": true
+        }
       }
     },
     "esprima": {
@@ -2669,12 +2768,20 @@
       }
     },
     "esrecurse": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
-      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
       "dev": true,
       "requires": {
-        "estraverse": "^4.1.0"
+        "estraverse": "^5.2.0"
+      },
+      "dependencies": {
+        "estraverse": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+          "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+          "dev": true
+        }
       }
     },
     "estraverse": {
@@ -5546,9 +5653,9 @@
       "dev": true
     },
     "node-abi": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz",
-      "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==",
+      "version": "2.19.1",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.1.tgz",
+      "integrity": "sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A==",
       "dev": true,
       "requires": {
         "semver": "^5.4.1"
@@ -6029,9 +6136,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz",
-      "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
+      "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
       "dev": true
     },
     "prettier-linter-helpers": {
@@ -7273,12 +7380,12 @@
       }
     },
     "tar-stream": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz",
-      "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==",
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.4.tgz",
+      "integrity": "sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==",
       "dev": true,
       "requires": {
-        "bl": "^4.0.1",
+        "bl": "^4.0.3",
         "end-of-stream": "^1.4.1",
         "fs-constants": "^1.0.0",
         "inherits": "^2.0.3",
@@ -7450,15 +7557,15 @@
       "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE="
     },
     "tree-sitter-cli": {
-      "version": "0.16.9",
-      "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.16.9.tgz",
-      "integrity": "sha512-Aguz2Ns7qG6t71MP9odhh4t9q3+f29BAmZq8XsTDMtoi5o/e9k+Umeqz6brNngCAz3vMBl1OX95ozdnYzhJWIA==",
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.17.0.tgz",
+      "integrity": "sha512-0osmcEvAsbp1cKx+rC0Ew8TzfqSCZgn7wvzRdgUcvF1k2Bk7vu/on3SHy1XG6El5HODwLZLAWfuRGay1DqBV5w==",
       "dev": true
     },
     "tree-sitter-elm": {
-      "version": "2.7.9",
-      "resolved": "https://registry.npmjs.org/tree-sitter-elm/-/tree-sitter-elm-2.7.9.tgz",
-      "integrity": "sha512-KefXeawSgERB+fGTrg6sFiDM14KMKxxLtp11i2lCUJPXlxMhliNc/kHgVFqXGKJYMZKp8EDApo2WGpg8kZQp5g==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/tree-sitter-elm/-/tree-sitter-elm-4.1.0.tgz",
+      "integrity": "sha512-uDJvTLoAHhkvP5iq3t3MGQWolgz918j70i+sRiM/Ul6Mqz5W4UYu/sVrWSVwuQ1A+Pna5QWxW5lDweZsOKnxWA==",
       "dev": true,
       "requires": {
         "nan": "^2.14.1",
@@ -7490,22 +7597,30 @@
       "dev": true
     },
     "ts-jest": {
-      "version": "26.3.0",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.3.0.tgz",
-      "integrity": "sha512-Jq2uKfx6bPd9+JDpZNMBJMdMQUC3sJ08acISj8NXlVgR2d5OqslEHOR2KHMgwymu8h50+lKIm0m0xj/ioYdW2Q==",
+      "version": "26.4.0",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.0.tgz",
+      "integrity": "sha512-ofBzoCqf6Nv/PoWb/ByV3VNKy2KJSikamOBxvR3E6eVdIw10GwAXoyvMWXXjZJK2s6S27ZE8fI+JBTnGaovl6Q==",
       "dev": true,
       "requires": {
         "@types/jest": "26.x",
         "bs-logger": "0.x",
         "buffer-from": "1.x",
         "fast-json-stable-stringify": "2.x",
-        "jest-util": "26.x",
+        "jest-util": "^26.1.0",
         "json5": "2.x",
         "lodash.memoize": "4.x",
         "make-error": "1.x",
         "mkdirp": "1.x",
         "semver": "7.x",
-        "yargs-parser": "18.x"
+        "yargs-parser": "20.x"
+      },
+      "dependencies": {
+        "yargs-parser": {
+          "version": "20.2.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.0.tgz",
+          "integrity": "sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A==",
+          "dev": true
+        }
       }
     },
     "tslib": {
@@ -7588,9 +7703,9 @@
       }
     },
     "typescript": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz",
-      "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==",
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+      "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
       "dev": true
     },
     "ultron": {
@@ -7907,9 +8022,9 @@
       }
     },
     "web-tree-sitter": {
-      "version": "0.16.4",
-      "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.16.4.tgz",
-      "integrity": "sha512-n1CfuJcJ+dynIx/fmavB6haPx37N3GZvY5HIGIselymDiSwNRC+8pAxOzoB4eVwUBJnbP3+aA8vWttrAZbgs7A=="
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.17.0.tgz",
+      "integrity": "sha512-rTg3Zp0IXg137y3azIZnYQXyKI7uYiT6fi6AqbaJ4NbVhOUQkm8VMrJ3H/kza2PyJ5eNitbS5I4T9ivbTXd1iQ=="
     },
     "webidl-conversions": {
       "version": "6.1.0",
diff --git a/package.json b/package.json
index 7a9f88f..7c2a6b6 100644
--- a/package.json
+++ b/package.json
@@ -26,25 +26,25 @@
     "vscode-languageserver": "^6.1.1",
     "vscode-languageserver-textdocument": "1.0.1",
     "vscode-uri": "^2.1.2",
-    "web-tree-sitter": "^0.16.4"
+    "web-tree-sitter": "^0.17.0"
   },
   "devDependencies": {
-    "@types/jest": "^26.0.10",
-    "@types/node": "14.6.1",
-    "@typescript-eslint/eslint-plugin": "^3.10.1",
-    "@typescript-eslint/parser": "^3.10.1",
+    "@types/jest": "^26.0.14",
+    "@types/node": "14.11.2",
+    "@typescript-eslint/eslint-plugin": "^4.2.0",
+    "@typescript-eslint/parser": "^4.2.0",
     "copyfiles": "^2.3.0",
     "doctoc": "^1.4.0",
+    "eslint": "^7.9.0",
     "eslint-config-prettier": "^6.11.0",
     "eslint-plugin-prettier": "^3.1.4",
-    "eslint": "^7.7.0",
-    "jest-mock-extended": "^1.0.10",
     "jest": "^26.4.2",
-    "prettier": "^2.1.1",
-    "tree-sitter-cli": "^0.16.9",
-    "tree-sitter-elm": "^2.7.9",
-    "ts-jest": "^26.3.0",
-    "typescript": "4.0.2"
+    "jest-mock-extended": "^1.0.10",
+    "prettier": "^2.1.2",
+    "tree-sitter-cli": "^0.17.0",
+    "tree-sitter-elm": "^4.1.0",
+    "ts-jest": "^26.4.0",
+    "typescript": "4.0.3"
   },
   "scripts": {
     "copy-wasm": "copyfiles ./tree-sitter-elm.wasm out",
diff --git a/src/elmWorkspace.ts b/src/elmWorkspace.ts
index 0a889cc..b1b291f 100644
--- a/src/elmWorkspace.ts
+++ b/src/elmWorkspace.ts
@@ -11,6 +11,7 @@ import { Forest } from "./forest";
 import { Imports } from "./imports";
 import * as utils from "./util/elmUtils";
 import { Settings } from "./util/settings";
+import { TypeCache } from "./util/types/typeCache";
 
 const readFile = util.promisify(fs.readFile);
 const readdir = util.promisify(fs.readdir);
@@ -30,6 +31,7 @@ export interface IElmWorkspace {
   getForest(): Forest;
   getImports(): Imports;
   getRootPath(): URI;
+  getTypeCache(): TypeCache;
 }
 
 export class ElmWorkspace implements IElmWorkspace {
@@ -43,6 +45,7 @@ export class ElmWorkspace implements IElmWorkspace {
   private parser: Parser;
   private connection: IConnection;
   private settings: Settings;
+  private typeCache: TypeCache;
 
   constructor(private rootPath: URI) {
     this.settings = container.resolve("Settings");
@@ -53,6 +56,7 @@ export class ElmWorkspace implements IElmWorkspace {
     );
 
     this.imports = new Imports();
+    this.typeCache = new TypeCache();
   }
 
   public async init(
@@ -87,6 +91,10 @@ export class ElmWorkspace implements IElmWorkspace {
     return this.rootPath;
   }
 
+  public getTypeCache(): TypeCache {
+    return this.typeCache;
+  }
+
   private async initWorkspace(
     progressCallback: (percent: number) => void,
   ): Promise<void> {
diff --git a/src/forest.ts b/src/forest.ts
index d20ed2c..3528876 100644
--- a/src/forest.ts
+++ b/src/forest.ts
@@ -40,9 +40,11 @@ export class Forest implements IForest {
   }
 
   public getExposingByModuleName(moduleName: string): IExposing[] | undefined {
-    const result = this.treeIndex.find(
-      (tree) => tree.moduleName === moduleName && tree.isExposed,
-    );
+    const result = this.treeIndex
+      .filter((tree) => tree.moduleName === moduleName)
+      .sort((x, y) => {
+        return x.isExposed === y.isExposed ? 0 : x.isExposed ? -1 : 1;
+      })[0];
     return result && result.exposing;
   }
 
@@ -55,9 +57,11 @@ export class Forest implements IForest {
   }
 
   public getByModuleName(moduleName: string): ITreeContainer | undefined {
-    return this.treeIndex.find(
-      (tree) => tree.moduleName === moduleName && tree.isExposed,
-    );
+    return this.treeIndex
+      .filter((tree) => tree.moduleName === moduleName)
+      .sort((x, y) => {
+        return x.isExposed === y.isExposed ? 0 : x.isExposed ? -1 : 1;
+      })[0];
   }
 
   public getByUri(uri: string): ITreeContainer | undefined {
diff --git a/src/imports.ts b/src/imports.ts
index c99b065..1300e61 100644
--- a/src/imports.ts
+++ b/src/imports.ts
@@ -191,6 +191,7 @@ export class Imports implements IImports {
     exposed.forEach((element) => {
       switch (element.type) {
         case "Function":
+        case "Port":
         case "TypeAlias":
           result.push({
             alias: `${importPrefix}.${element.name}`,
diff --git a/src/providers/astProvider.ts b/src/providers/astProvider.ts
index 790ad59..5d57e7c 100644
--- a/src/providers/astProvider.ts
+++ b/src/providers/astProvider.ts
@@ -5,9 +5,11 @@ import {
   DidOpenTextDocumentParams,
   IConnection,
   VersionedTextDocumentIdentifier,
+  Event,
+  Emitter,
 } from "vscode-languageserver";
 import { URI } from "vscode-uri";
-import Parser, { Tree } from "web-tree-sitter";
+import Parser, { Tree, SyntaxNode } from "web-tree-sitter";
 import { IElmWorkspace } from "../elmWorkspace";
 import { IDocumentEvents } from "../util/documentEvents";
 import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher";
@@ -17,6 +19,10 @@ export class ASTProvider {
   private connection: IConnection;
   private parser: Parser;
 
+  private treeChangeEvent = new Emitter<{ uri: string; tree: Tree }>();
+  readonly onTreeChange: Event<{ uri: string; tree: Tree }> = this
+    .treeChangeEvent.event;
+
   constructor() {
     this.parser = container.resolve("Parser");
     this.connection = container.resolve<IConnection>("Connection");
@@ -87,6 +93,8 @@ export class ASTProvider {
 
       // Refresh imports of the calling file
       imports.updateImports(document.uri, tree, forest);
+
+      this.treeChangeEvent.fire({ uri: document.uri, tree });
     }
   };
 }
diff --git a/src/providers/codeActionProvider.ts b/src/providers/codeActionProvider.ts
index c967557..1fe7600 100644
--- a/src/providers/codeActionProvider.ts
+++ b/src/providers/codeActionProvider.ts
@@ -16,6 +16,7 @@ import { IClientSettings, Settings } from "../util/settings";
 import { TreeUtils } from "../util/treeUtils";
 import { ElmAnalyseDiagnostics } from "./diagnostics/elmAnalyseDiagnostics";
 import { ElmMakeDiagnostics } from "./diagnostics/elmMakeDiagnostics";
+import { TypeInferenceDiagnostics } from "./diagnostics/typeInferenceDiagnostics";
 import { ExposeUnexposeHandler } from "./handlers/exposeUnexposeHandler";
 import { MoveRefactoringHandler } from "./handlers/moveRefactoringHandler";
 
@@ -24,6 +25,7 @@ export class CodeActionProvider {
   private settings: Settings;
   private elmAnalyse: ElmAnalyseDiagnostics | null = null;
   private elmMake: ElmMakeDiagnostics;
+  private functionTypeAnnotationDiagnostics: TypeInferenceDiagnostics;
   private clientSettings: IClientSettings;
 
   constructor() {
@@ -35,6 +37,9 @@ export class CodeActionProvider {
       );
     }
     this.elmMake = container.resolve<ElmMakeDiagnostics>(ElmMakeDiagnostics);
+    this.functionTypeAnnotationDiagnostics = container.resolve<
+      TypeInferenceDiagnostics
+    >(TypeInferenceDiagnostics);
     this.connection = container.resolve<IConnection>("Connection");
 
     this.onCodeAction = this.onCodeAction.bind(this);
@@ -61,9 +66,13 @@ export class CodeActionProvider {
     const analyse =
       (this.elmAnalyse && this.elmAnalyse.onCodeAction(params)) ?? [];
     const make = this.elmMake.onCodeAction(params);
+    const typeAnnotation = this.functionTypeAnnotationDiagnostics.onCodeAction(
+      params,
+    );
     return [
       ...analyse,
       ...make,
+      ...typeAnnotation,
       ...this.getRefactorCodeActions(params, elmWorkspace),
     ];
   }
diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts
index 733046f..2cd495e 100644
--- a/src/providers/completionProvider.ts
+++ b/src/providers/completionProvider.ts
@@ -291,11 +291,12 @@ export class CompletionProvider {
           if (parent?.type === "value_qid") {
             // Qualified submodule and value access
             targetNode = contextNode.previousNamedSibling;
-          } else if (parent?.type === "field_access_segment") {
+          } else if (parent?.type === "field_access_expr") {
             // Record field access
             targetNode =
-              parent?.previousNamedSibling?.lastNamedChild?.lastNamedChild ??
-              parent.previousNamedSibling?.lastNamedChild;
+              contextNode?.previousNamedSibling?.lastNamedChild
+                ?.lastNamedChild ??
+              contextNode.previousNamedSibling?.lastNamedChild;
           } else if (parent?.type === "upper_case_qid") {
             // Imports
             targetNode = contextNode.previousNamedSibling;
@@ -883,33 +884,30 @@ export class CompletionProvider {
     const sortPrefix = "a";
     if (node.parent) {
       if (node.parent.type === "let_in_expr") {
-        const letNode = TreeUtils.findFirstNamedChildOfType("let", node.parent);
-        if (letNode) {
-          letNode.children.forEach((nodeToProcess) => {
-            if (
-              nodeToProcess &&
-              nodeToProcess.type === "value_declaration" &&
-              nodeToProcess.firstNamedChild !== null &&
-              nodeToProcess.firstNamedChild.type ===
-                "function_declaration_left" &&
-              nodeToProcess.firstNamedChild.firstNamedChild !== null &&
-              nodeToProcess.firstNamedChild.firstNamedChild.type ===
-                "lower_case_identifier"
-            ) {
-              const markdownDocumentation = HintHelper.createHintFromDefinitionInLet(
-                nodeToProcess,
-              );
-              result.push(
-                this.createFunctionCompletion({
-                  markdownDocumentation,
-                  label: nodeToProcess.firstNamedChild.firstNamedChild.text,
-                  range,
-                  sortPrefix,
-                }),
-              );
-            }
-          });
-        }
+        node.parent.children.forEach((nodeToProcess) => {
+          if (
+            nodeToProcess &&
+            nodeToProcess.type === "value_declaration" &&
+            nodeToProcess.firstNamedChild !== null &&
+            nodeToProcess.firstNamedChild.type ===
+              "function_declaration_left" &&
+            nodeToProcess.firstNamedChild.firstNamedChild !== null &&
+            nodeToProcess.firstNamedChild.firstNamedChild.type ===
+              "lower_case_identifier"
+          ) {
+            const markdownDocumentation = HintHelper.createHintFromDefinitionInLet(
+              nodeToProcess,
+            );
+            result.push(
+              this.createFunctionCompletion({
+                markdownDocumentation,
+                label: nodeToProcess.firstNamedChild.firstNamedChild.text,
+                range,
+                sortPrefix,
+              }),
+            );
+          }
+        });
       }
       if (
         node.parent.type === "case_of_branch" &&
@@ -1092,7 +1090,7 @@ export class CompletionProvider {
       alreadyImported = false;
     }
 
-    if (moduleTree) {
+    if (moduleTree && moduleTree.isExposed) {
       // Get exposed values
       const imports = ImportUtils.getPossibleImportsOfTree(moduleTree);
       imports.forEach((value) => {
diff --git a/src/providers/diagnostics/diagnosticsProvider.ts b/src/providers/diagnostics/diagnosticsProvider.ts
index dd2669b..9c7520c 100644
--- a/src/providers/diagnostics/diagnosticsProvider.ts
+++ b/src/providers/diagnostics/diagnosticsProvider.ts
@@ -9,6 +9,9 @@ import { NoWorkspaceContainsError } from "../../util/noWorkspaceContainsError";
 import { ElmAnalyseTrigger } from "../../util/settings";
 import { TextDocumentEvents } from "../../util/textDocumentEvents";
 import { ElmMakeDiagnostics } from "./elmMakeDiagnostics";
+import { TypeInferenceDiagnostics } from "./typeInferenceDiagnostics";
+import { ASTProvider } from "../astProvider";
+import { IElmWorkspace } from "src/elmWorkspace";
 
 export interface IElmIssueRegion {
   start: { line: number; column: number };
@@ -29,16 +32,19 @@ export interface IElmIssue {
 export class DiagnosticsProvider {
   private elmMakeDiagnostics: ElmMakeDiagnostics;
   private elmAnalyseDiagnostics: ElmAnalyseDiagnostics | null = null;
-  private elmWorkspaceMatcher: ElmWorkspaceMatcher<TextDocument>;
+  private typeInferenceDiagnostics: TypeInferenceDiagnostics;
+  private elmWorkspaceMatcher: ElmWorkspaceMatcher<{ uri: string }>;
   private currentDiagnostics: {
     elmMake: Map<string, Diagnostic[]>;
     elmAnalyse: Map<string, Diagnostic[]>;
     elmTest: Map<string, Diagnostic[]>;
+    typeInference: Map<string, Diagnostic[]>;
   };
   private events: TextDocumentEvents;
   private connection: IConnection;
   private settings: Settings;
   private clientSettings: IClientSettings;
+  private workspaces: IElmWorkspace[];
 
   constructor() {
     this.settings = container.resolve("Settings");
@@ -51,16 +57,24 @@ export class DiagnosticsProvider {
     this.elmMakeDiagnostics = container.resolve<ElmMakeDiagnostics>(
       ElmMakeDiagnostics,
     );
+    this.typeInferenceDiagnostics = container.resolve<TypeInferenceDiagnostics>(
+      TypeInferenceDiagnostics,
+    );
     this.connection = container.resolve<IConnection>("Connection");
     this.events = container.resolve<TextDocumentEvents>(TextDocumentEvents);
     this.newElmAnalyseDiagnostics = this.newElmAnalyseDiagnostics.bind(this);
     this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((doc) =>
       URI.parse(doc.uri),
     );
+    this.workspaces = container.resolve("ElmWorkspaces");
+
+    const astProvider = container.resolve<ASTProvider>(ASTProvider);
+
     this.currentDiagnostics = {
       elmAnalyse: new Map<string, Diagnostic[]>(),
       elmMake: new Map<string, Diagnostic[]>(),
       elmTest: new Map<string, Diagnostic[]>(),
+      typeInference: new Map<string, Diagnostic[]>(),
     };
     // register onChange listener if settings are not on-save only
     void this.settings.getClientSettings().then(({ elmAnalyseTrigger }) => {
@@ -78,6 +92,7 @@ export class DiagnosticsProvider {
           this.currentDiagnostics.elmAnalyse.delete(uri);
           this.currentDiagnostics.elmMake.delete(uri);
           this.currentDiagnostics.elmTest.delete(uri);
+          this.currentDiagnostics.typeInference.delete(uri);
         });
         this.sendDiagnostics();
       });
@@ -92,6 +107,46 @@ export class DiagnosticsProvider {
           this.getDiagnostics(d, false, elmAnalyseTrigger),
         );
       }
+
+      this.workspaces.forEach((workspace) => {
+        workspace.getForest().treeIndex.forEach((treeContainer) => {
+          if (treeContainer.writeable) {
+            const treeDiagnostics = this.typeInferenceDiagnostics.createDiagnostics(
+              treeContainer.tree,
+              treeContainer.uri,
+              workspace,
+            );
+
+            this.currentDiagnostics.typeInference.set(
+              treeContainer.uri,
+              treeDiagnostics,
+            );
+          }
+        });
+      });
+
+      this.sendDiagnostics();
+
+      astProvider.onTreeChange(({ uri, tree }) => {
+        let workspace;
+        try {
+          workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor({ uri });
+        } catch (error) {
+          if (error instanceof NoWorkspaceContainsError) {
+            this.connection.console.info(error.message);
+            return; // ignore file that doesn't correspond to a workspace
+          }
+
+          throw error;
+        }
+
+        this.currentDiagnostics.typeInference.set(
+          uri,
+          this.typeInferenceDiagnostics.createDiagnostics(tree, uri, workspace),
+        );
+
+        this.sendDiagnostics();
+      });
     });
   }
 
@@ -102,26 +157,52 @@ export class DiagnosticsProvider {
     this.sendDiagnostics();
   }
 
-  private sendDiagnostics(): void {
-    const allDiagnostics = new Map<string, Diagnostic[]>();
+  private addOrMergeDiagnostics(
+    map1: Map<string, Diagnostic[]>,
+    map2: Map<string, Diagnostic[]>,
+  ): Map<string, Diagnostic[]> {
+    const result = new Map<string, Diagnostic[]>(map1);
 
-    for (const [uri, diagnostics] of this.currentDiagnostics.elmMake) {
-      allDiagnostics.set(uri, diagnostics);
-    }
+    for (const key of map2.keys()) {
+      const value = map2.get(key);
 
-    for (const [uri, diagnostics] of this.currentDiagnostics.elmTest) {
-      const currentDiagnostics = allDiagnostics.get(uri) ?? [];
-      if (currentDiagnostics.length === 0) {
-        allDiagnostics.set(uri, diagnostics);
+      if (value) {
+        if (map1.has(key)) {
+          const value1 = map1.get(key);
+          if (value1) {
+            result.set(key, [...value, ...value1]);
+          }
+        } else {
+          result.set(key, value);
+        }
       }
     }
 
-    for (const [uri, diagnostics] of this.currentDiagnostics.elmAnalyse) {
-      const currentDiagnostics = allDiagnostics.get(uri) ?? [];
-      if (currentDiagnostics.length === 0) {
-        allDiagnostics.set(uri, diagnostics);
-      }
-    }
+    return result;
+  }
+
+  private sendDiagnostics(): void {
+    let allDiagnostics = new Map<string, Diagnostic[]>();
+
+    allDiagnostics = this.addOrMergeDiagnostics(
+      allDiagnostics,
+      this.currentDiagnostics.elmMake,
+    );
+
+    allDiagnostics = this.addOrMergeDiagnostics(
+      allDiagnostics,
+      this.currentDiagnostics.elmTest,
+    );
+
+    allDiagnostics = this.addOrMergeDiagnostics(
+      allDiagnostics,
+      this.currentDiagnostics.elmAnalyse,
+    );
+
+    allDiagnostics = this.addOrMergeDiagnostics(
+      allDiagnostics,
+      this.currentDiagnostics.typeInference,
+    );
 
     for (const [uri, diagnostics] of allDiagnostics) {
       this.connection.sendDiagnostics({ uri, diagnostics });
@@ -140,8 +221,9 @@ export class DiagnosticsProvider {
     );
 
     const uri = URI.parse(document.uri);
+    let workspace;
     try {
-      this.elmWorkspaceMatcher.getElmWorkspaceFor(document);
+      workspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(document);
     } catch (error) {
       if (error instanceof NoWorkspaceContainsError) {
         this.connection.console.info(error.message);
diff --git a/src/providers/diagnostics/typeInferenceDiagnostics.ts b/src/providers/diagnostics/typeInferenceDiagnostics.ts
new file mode 100644
index 0000000..188b338
--- /dev/null
+++ b/src/providers/diagnostics/typeInferenceDiagnostics.ts
@@ -0,0 +1,164 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+import {
+  CodeAction,
+  CodeActionKind,
+  CodeActionParams,
+  Diagnostic,
+  DiagnosticSeverity,
+  Range,
+  TextEdit,
+} from "vscode-languageserver";
+import { IElmWorkspace } from "../../elmWorkspace";
+import { findType } from "../../util/types/typeInference";
+import { SyntaxNode, Tree } from "web-tree-sitter";
+import { PositionUtil } from "../../positionUtil";
+import { TreeUtils } from "../../util/treeUtils";
+import { Utils } from "../../util/utils";
+import { URI } from "vscode-uri";
+import { ElmWorkspaceMatcher } from "../../util/elmWorkspaceMatcher";
+import { TypeRenderer } from "../../util/types/typeRenderer";
+
+export class TypeInferenceDiagnostics {
+  TYPE_INFERENCE = "Type Inference";
+
+  private elmWorkspaceMatcher: ElmWorkspaceMatcher<URI>;
+
+  constructor() {
+    this.elmWorkspaceMatcher = new ElmWorkspaceMatcher((uri) => uri);
+  }
+
+  public createDiagnostics = (
+    tree: Tree,
+    uri: string,
+    elmWorkspace: IElmWorkspace,
+  ): Diagnostic[] => {
+    let diagnostics: Diagnostic[] = [];
+
+    const allTopLevelFunctions = TreeUtils.findAllTopLevelFunctionDeclarationsWithoutTypeAnnotation(
+      tree,
+    );
+
+    if (allTopLevelFunctions) {
+      const inferencedTypes = allTopLevelFunctions
+        .filter(Utils.notUndefinedOrNull)
+        .map((func) => func.firstChild?.firstChild)
+        .filter(Utils.notUndefinedOrNull)
+        .flatMap((node) => {
+          try {
+            const typeString: string = TypeRenderer.typeToString(
+              findType(node, uri, elmWorkspace),
+              node.tree,
+              uri,
+              elmWorkspace.getImports(),
+            );
+
+            if (typeString && typeString !== "Unknown" && node) {
+              return {
+                range: this.getNodeRange(node),
+                message: `Missing type annotation: \`${typeString}\``,
+                severity: DiagnosticSeverity.Information,
+                source: this.TYPE_INFERENCE,
+              };
+            }
+          } catch (e) {
+            console.log(e);
+          }
+        })
+        .filter(Utils.notUndefined.bind(this));
+
+      diagnostics = inferencedTypes ?? [];
+    }
+
+    return diagnostics;
+  };
+
+  public onCodeAction(params: CodeActionParams): CodeAction[] {
+    const { uri } = params.textDocument;
+    const typeInferenceDiagnostics: Diagnostic[] = this.filterTypeInferenceDiagnostics(
+      params.context.diagnostics,
+    );
+
+    return this.convertDiagnosticsToCodeActions(typeInferenceDiagnostics, uri);
+  }
+
+  private filterTypeInferenceDiagnostics(
+    diagnostics: Diagnostic[],
+  ): Diagnostic[] {
+    return diagnostics.filter(
+      (diagnostic) => diagnostic.source === this.TYPE_INFERENCE,
+    );
+  }
+
+  private convertDiagnosticsToCodeActions(
+    diagnostics: Diagnostic[],
+    uri: string,
+  ): CodeAction[] {
+    const result: CodeAction[] = [];
+
+    const elmWorkspace = this.elmWorkspaceMatcher.getElmWorkspaceFor(
+      URI.parse(uri),
+    );
+
+    const forest = elmWorkspace.getForest();
+
+    const treeContainer = forest.getByUri(uri);
+
+    if (treeContainer) {
+      diagnostics.forEach((diagnostic) => {
+        const nodeAtPosition = TreeUtils.getNamedDescendantForPosition(
+          treeContainer.tree.rootNode,
+          diagnostic.range.start,
+        );
+
+        const typeString: string = TypeRenderer.typeToString(
+          findType(nodeAtPosition, uri, elmWorkspace),
+          nodeAtPosition.tree,
+          uri,
+          elmWorkspace.getImports(),
+        );
+
+        result.push(
+          this.insertQuickFixAtStart(
+            uri,
+            `${nodeAtPosition.text} : ${typeString}\n`,
+            diagnostic,
+            "Add inferred annotation",
+          ),
+        );
+      });
+    }
+    return result;
+  }
+
+  private insertQuickFixAtStart(
+    uri: string,
+    replaceWith: string,
+    diagnostic: Diagnostic,
+    title: string,
+  ): CodeAction {
+    const map: {
+      [uri: string]: TextEdit[];
+    } = {};
+    if (!map[uri]) {
+      map[uri] = [];
+    }
+    map[uri].push(TextEdit.insert(diagnostic.range.start, replaceWith));
+    return {
+      diagnostics: [diagnostic],
+      edit: { changes: map },
+      kind: CodeActionKind.QuickFix,
+      title,
+    };
+  }
+
+  private getNodeRange(node: SyntaxNode): Range {
+    const end = PositionUtil.FROM_TS_POSITION(node.endPosition).toVSPosition();
+    return {
+      start: PositionUtil.FROM_TS_POSITION(node.startPosition).toVSPosition(),
+      end: {
+        ...end,
+        character: end.character,
+      },
+    };
+  }
+}
diff --git a/src/providers/foldingProvider.ts b/src/providers/foldingProvider.ts
index cb7335e..cbbb471 100644
--- a/src/providers/foldingProvider.ts
+++ b/src/providers/foldingProvider.ts
@@ -18,11 +18,6 @@ export class FoldingRangeProvider {
     "type_declaration",
     "record_expr",
     "case_of_branch",
-    "let",
-    "in",
-    "if",
-    "then",
-    "else",
   ]);
   private connection: IConnection;
   constructor() {
@@ -75,6 +70,44 @@ export class FoldingRangeProvider {
               startLine: node.startPosition.row,
             });
           }
+        } else if (node.type === "let_in_expr") {
+          // Use fields in the future
+          const valueDeclarations = node.namedChildren.filter(
+            (n) => n.type === "value_declaration",
+          );
+          const lastValueDeclaration =
+            valueDeclarations[valueDeclarations.length - 1];
+          const letBody = node.lastNamedChild;
+
+          if (lastValueDeclaration) {
+            folds.push({
+              endCharacter: lastValueDeclaration.endPosition.column,
+              endLine: lastValueDeclaration.endPosition.row,
+              kind: FoldingRangeKind.Region,
+              startCharacter: node.startPosition.column,
+              startLine: node.startPosition.row,
+            });
+          }
+
+          if (letBody) {
+            folds.push({
+              endCharacter: node.endPosition.column,
+              endLine: node.endPosition.row,
+              kind: FoldingRangeKind.Region,
+              startCharacter: letBody.startPosition.column,
+              startLine: letBody.startPosition.row - 1,
+            });
+          }
+        } else if (node.type === "if_else_expr") {
+          node.namedChildren.slice(1).forEach((child) => {
+            folds.push({
+              endCharacter: child.endPosition.column,
+              endLine: child.endPosition.row,
+              kind: FoldingRangeKind.Region,
+              startCharacter: child.startPosition.column,
+              startLine: child.startPosition.row - 1,
+            });
+          });
         } else if (this.REGION_CONSTRUCTS.has(node.type)) {
           folds.push({
             endCharacter: node.endPosition.column,
diff --git a/src/providers/hoverProvider.ts b/src/providers/hoverProvider.ts
index a6ef97a..239d8b4 100644
--- a/src/providers/hoverProvider.ts
+++ b/src/providers/hoverProvider.ts
@@ -6,7 +6,7 @@ import {
   TextDocumentPositionParams,
 } from "vscode-languageserver";
 import { URI } from "vscode-uri";
-import { SyntaxNode, Tree } from "web-tree-sitter";
+import { SyntaxNode } from "web-tree-sitter";
 import { IElmWorkspace } from "../elmWorkspace";
 import { getEmptyTypes } from "../util/elmUtils";
 import { ElmWorkspaceMatcher } from "../util/elmWorkspaceMatcher";
@@ -34,7 +34,7 @@ export class HoverProvider {
     this.connection.console.info(`A hover was requested`);
 
     const forest = elmWorkspace.getForest();
-    const tree: Tree | undefined = forest.getTree(params.textDocument.uri);
+    const tree = forest.getTree(params.textDocument.uri);
 
     if (tree) {
       const nodeAtPosition = TreeUtils.getNamedDescendantForPosition(
diff --git a/src/providers/index.ts b/src/providers/index.ts
index 0e6287f..702216a 100644
--- a/src/providers/index.ts
+++ b/src/providers/index.ts
@@ -6,6 +6,7 @@ export * from "./definitionProvider";
 export * from "./diagnostics/diagnosticsProvider";
 export * from "./diagnostics/elmAnalyseDiagnostics";
 export * from "./diagnostics/elmMakeDiagnostics";
+export * from "./diagnostics/typeInferenceDiagnostics";
 export * from "./documentFormatingProvider";
 export * from "./documentSymbolProvider";
 export * from "./selectionRangeProvider";
diff --git a/src/server.ts b/src/server.ts
index a91683a..b5c557a 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -22,6 +22,7 @@ import {
   ElmAnalyseDiagnostics,
   ElmMakeDiagnostics,
   FoldingRangeProvider,
+  TypeInferenceDiagnostics,
   HoverProvider,
   ReferencesProvider,
   RenameProvider,
@@ -130,6 +131,10 @@ export class Server implements ILanguageServer {
       useValue: new ElmMakeDiagnostics(),
     });
 
+    container.register(TypeInferenceDiagnostics, {
+      useValue: new TypeInferenceDiagnostics(),
+    });
+
     const clientSettings = await settings.getClientSettings();
 
     container.register("ClientSettings", {
@@ -143,12 +148,14 @@ export class Server implements ILanguageServer {
           : null,
     });
 
+    container.register(ASTProvider, {
+      useValue: new ASTProvider(),
+    });
+
     new DiagnosticsProvider();
 
     new CodeActionProvider();
 
-    new ASTProvider();
-
     new FoldingRangeProvider();
     new CompletionProvider();
     new HoverProvider();
diff --git a/src/util/hintHelper.ts b/src/util/hintHelper.ts
index dfba77c..f0daafd 100644
--- a/src/util/hintHelper.ts
+++ b/src/util/hintHelper.ts
@@ -164,7 +164,7 @@ export class HintHelper {
     return newComment.trim();
   }
 
-  private static wrapCodeInMarkdown(code: string): string {
+  public static wrapCodeInMarkdown(code: string): string {
     return `\n\`\`\`elm\n${code}\n\`\`\`\n`;
   }
 }
diff --git a/src/util/references.ts b/src/util/references.ts
index f4f55c7..d68c87a 100644
--- a/src/util/references.ts
+++ b/src/util/references.ts
@@ -19,118 +19,120 @@ export class References {
         const moduleNameNode = TreeUtils.getModuleNameNode(refSourceTree.tree);
         switch (definitionNode.nodeType) {
           case "Function":
-            const annotationNameNode = this.getFunctionAnnotationNameNodeFromDefinition(
-              definitionNode.node,
-            );
-            if (annotationNameNode && refSourceTree.writeable) {
-              references.push({
-                node: annotationNameNode,
-                uri: definitionNode.uri,
-              });
-            }
-
-            const functionNameNode = TreeUtils.getFunctionNameNodeFromDefinition(
-              definitionNode.node,
-            );
-            if (functionNameNode) {
-              if (refSourceTree.writeable) {
+            {
+              const annotationNameNode = this.getFunctionAnnotationNameNodeFromDefinition(
+                definitionNode.node,
+              );
+              if (annotationNameNode && refSourceTree.writeable) {
                 references.push({
-                  node: functionNameNode,
+                  node: annotationNameNode,
                   uri: definitionNode.uri,
                 });
               }
 
-              const localFunctions =
-                definitionNode.node.parent &&
-                definitionNode.node.parent.type === "let" &&
-                definitionNode.node.parent.nextNamedSibling
-                  ? this.findFunctionCalls(
-                      definitionNode.node.parent.nextNamedSibling,
-                      functionNameNode.text,
-                    )
-                  : this.findFunctionCalls(
-                      refSourceTree.tree.rootNode,
-                      functionNameNode.text,
-                    );
+              const functionNameNode = TreeUtils.getFunctionNameNodeFromDefinition(
+                definitionNode.node,
+              );
+              if (functionNameNode) {
+                if (refSourceTree.writeable) {
+                  references.push({
+                    node: functionNameNode,
+                    uri: definitionNode.uri,
+                  });
+                }
 
-              if (localFunctions && refSourceTree.writeable) {
-                references.push(
-                  ...localFunctions.map((node) => {
-                    return { node, uri: definitionNode.uri };
-                  }),
-                );
-              }
+                const localFunctions =
+                  definitionNode.node.parent?.parent &&
+                  definitionNode.node.parent?.parent.type === "let_in_expr" &&
+                  definitionNode.node.parent?.parent.lastNamedChild
+                    ? this.findFunctionCalls(
+                        definitionNode.node.parent.parent.lastNamedChild,
+                        functionNameNode.text,
+                      )
+                    : this.findFunctionCalls(
+                        refSourceTree.tree.rootNode,
+                        functionNameNode.text,
+                      );
 
-              if (
-                TreeUtils.isExposedFunction(
-                  refSourceTree.tree,
-                  functionNameNode.text,
-                )
-              ) {
-                const moduleDeclarationNode = TreeUtils.findModuleDeclaration(
-                  refSourceTree.tree,
-                );
-                if (moduleDeclarationNode) {
-                  const exposedNode = TreeUtils.findExposedFunctionNode(
-                    moduleDeclarationNode,
+                if (localFunctions && refSourceTree.writeable) {
+                  references.push(
+                    ...localFunctions.map((node) => {
+                      return { node, uri: definitionNode.uri };
+                    }),
+                  );
+                }
+
+                if (
+                  TreeUtils.isExposedFunction(
+                    refSourceTree.tree,
                     functionNameNode.text,
+                  )
+                ) {
+                  const moduleDeclarationNode = TreeUtils.findModuleDeclaration(
+                    refSourceTree.tree,
                   );
+                  if (moduleDeclarationNode) {
+                    const exposedNode = TreeUtils.findExposedFunctionNode(
+                      moduleDeclarationNode,
+                      functionNameNode.text,
+                    );
 
-                  if (exposedNode && refSourceTree.writeable) {
-                    references.push({
-                      node: exposedNode,
-                      uri: definitionNode.uri,
-                    });
+                    if (exposedNode && refSourceTree.writeable) {
+                      references.push({
+                        node: exposedNode,
+                        uri: definitionNode.uri,
+                      });
+                    }
                   }
-                }
 
-                if (moduleNameNode) {
-                  for (const uri in imports.imports) {
-                    if (imports.imports.hasOwnProperty(uri)) {
-                      const element = imports.imports[uri];
-                      const needsToBeChecked = element.filter(
-                        (a) =>
-                          uri !== definitionNode.uri &&
-                          a.fromModuleName === moduleNameNode.text &&
-                          a.type === "Function" &&
-                          (a.alias.endsWith(`.${functionNameNode.text}`) ||
-                            a.alias === functionNameNode.text),
-                      );
-                      if (needsToBeChecked.length > 0) {
-                        const treeToCheck = forest.getByUri(uri);
-
-                        if (treeToCheck && treeToCheck.writeable) {
-                          const importClauseNode = TreeUtils.findImportClauseByName(
-                            treeToCheck.tree,
-                            moduleNameNode.text,
-                          );
-                          if (importClauseNode) {
-                            const exposedNode = TreeUtils.findExposedFunctionNode(
-                              importClauseNode,
-                              functionNameNode.text,
+                  if (moduleNameNode) {
+                    for (const uri in imports.imports) {
+                      if (imports.imports.hasOwnProperty(uri)) {
+                        const element = imports.imports[uri];
+                        const needsToBeChecked = element.filter(
+                          (a) =>
+                            uri !== definitionNode.uri &&
+                            a.fromModuleName === moduleNameNode.text &&
+                            a.type === "Function" &&
+                            (a.alias.endsWith(`.${functionNameNode.text}`) ||
+                              a.alias === functionNameNode.text),
+                        );
+                        if (needsToBeChecked.length > 0) {
+                          const treeToCheck = forest.getByUri(uri);
+
+                          if (treeToCheck && treeToCheck.writeable) {
+                            const importClauseNode = TreeUtils.findImportClauseByName(
+                              treeToCheck.tree,
+                              moduleNameNode.text,
                             );
+                            if (importClauseNode) {
+                              const exposedNode = TreeUtils.findExposedFunctionNode(
+                                importClauseNode,
+                                functionNameNode.text,
+                              );
 
-                            if (exposedNode) {
-                              references.push({
-                                node: exposedNode,
-                                uri,
-                              });
+                              if (exposedNode) {
+                                references.push({
+                                  node: exposedNode,
+                                  uri,
+                                });
+                              }
                             }
-                          }
 
-                          needsToBeChecked.forEach((a) => {
-                            const functions = this.findFunctionCalls(
-                              treeToCheck.tree.rootNode,
-                              a.alias,
-                            );
-                            if (functions) {
-                              references.push(
-                                ...functions.map((node) => {
-                                  return { node, uri };
-                                }),
+                            needsToBeChecked.forEach((a) => {
+                              const functions = this.findFunctionCalls(
+                                treeToCheck.tree.rootNode,
+                                a.alias,
                               );
-                            }
-                          });
+                              if (functions) {
+                                references.push(
+                                  ...functions.map((node) => {
+                                    return { node, uri };
+                                  }),
+                                );
+                              }
+                            });
+                          }
                         }
                       }
                     }
@@ -142,102 +144,104 @@ export class References {
             break;
           case "Type":
           case "TypeAlias":
-            const typeOrTypeAliasNameNode = TreeUtils.getTypeOrTypeAliasNameNodeFromDefinition(
-              definitionNode.node,
-            );
-
-            if (typeOrTypeAliasNameNode) {
-              if (refSourceTree.writeable) {
-                references.push({
-                  node: typeOrTypeAliasNameNode,
-                  uri: definitionNode.uri,
-                });
-              }
-
-              const localFunctions = TreeUtils.findTypeOrTypeAliasCalls(
-                refSourceTree.tree,
-                typeOrTypeAliasNameNode.text,
+            {
+              const typeOrTypeAliasNameNode = TreeUtils.getTypeOrTypeAliasNameNodeFromDefinition(
+                definitionNode.node,
               );
-              if (localFunctions && refSourceTree.writeable) {
-                references.push(
-                  ...localFunctions.map((node) => {
-                    return { node, uri: definitionNode.uri };
-                  }),
-                );
-              }
 
-              if (
-                TreeUtils.isExposedTypeOrTypeAlias(
+              if (typeOrTypeAliasNameNode) {
+                if (refSourceTree.writeable) {
+                  references.push({
+                    node: typeOrTypeAliasNameNode,
+                    uri: definitionNode.uri,
+                  });
+                }
+
+                const localFunctions = TreeUtils.findTypeOrTypeAliasCalls(
                   refSourceTree.tree,
                   typeOrTypeAliasNameNode.text,
-                )
-              ) {
-                const moduleDeclarationNode = TreeUtils.findModuleDeclaration(
-                  refSourceTree.tree,
                 );
-                if (moduleDeclarationNode) {
-                  const exposedNode = TreeUtils.findExposedTypeOrTypeAliasNode(
-                    moduleDeclarationNode,
+                if (localFunctions && refSourceTree.writeable) {
+                  references.push(
+                    ...localFunctions.map((node) => {
+                      return { node, uri: definitionNode.uri };
+                    }),
+                  );
+                }
+
+                if (
+                  TreeUtils.isExposedTypeOrTypeAlias(
+                    refSourceTree.tree,
                     typeOrTypeAliasNameNode.text,
+                  )
+                ) {
+                  const moduleDeclarationNode = TreeUtils.findModuleDeclaration(
+                    refSourceTree.tree,
                   );
+                  if (moduleDeclarationNode) {
+                    const exposedNode = TreeUtils.findExposedTypeOrTypeAliasNode(
+                      moduleDeclarationNode,
+                      typeOrTypeAliasNameNode.text,
+                    );
 
-                  if (exposedNode && refSourceTree.writeable) {
-                    references.push({
-                      node: exposedNode,
-                      uri: definitionNode.uri,
-                    });
+                    if (exposedNode && refSourceTree.writeable) {
+                      references.push({
+                        node: exposedNode,
+                        uri: definitionNode.uri,
+                      });
+                    }
                   }
-                }
-
-                if (moduleNameNode) {
-                  for (const uri in imports.imports) {
-                    if (imports.imports.hasOwnProperty(uri)) {
-                      const element = imports.imports[uri];
-                      const needsToBeChecked = element.filter(
-                        (a) =>
-                          uri !== definitionNode.uri &&
-                          a.fromModuleName === moduleNameNode.text &&
-                          (a.type === "Type" || a.type === "TypeAlias") &&
-                          (a.alias.endsWith(
-                            `.${typeOrTypeAliasNameNode.text}`,
-                          ) ||
-                            a.alias === typeOrTypeAliasNameNode.text),
-                      );
-                      if (needsToBeChecked.length > 0) {
-                        const treeToCheck = forest.getByUri(uri);
-
-                        if (treeToCheck && treeToCheck.writeable) {
-                          const importClauseNode = TreeUtils.findImportClauseByName(
-                            treeToCheck.tree,
-                            moduleNameNode.text,
-                          );
-                          if (importClauseNode) {
-                            const exposedNode = TreeUtils.findExposedTypeOrTypeAliasNode(
-                              importClauseNode,
-                              typeOrTypeAliasNameNode.text,
-                            );
 
-                            if (exposedNode) {
-                              references.push({
-                                node: exposedNode,
-                                uri,
-                              });
-                            }
-                          }
+                  if (moduleNameNode) {
+                    for (const uri in imports.imports) {
+                      if (imports.imports.hasOwnProperty(uri)) {
+                        const element = imports.imports[uri];
+                        const needsToBeChecked = element.filter(
+                          (a) =>
+                            uri !== definitionNode.uri &&
+                            a.fromModuleName === moduleNameNode.text &&
+                            (a.type === "Type" || a.type === "TypeAlias") &&
+                            (a.alias.endsWith(
+                              `.${typeOrTypeAliasNameNode.text}`,
+                            ) ||
+                              a.alias === typeOrTypeAliasNameNode.text),
+                        );
+                        if (needsToBeChecked.length > 0) {
+                          const treeToCheck = forest.getByUri(uri);
 
-                          needsToBeChecked.forEach((a) => {
-                            const typeOrTypeAliasCalls = TreeUtils.findTypeOrTypeAliasCalls(
+                          if (treeToCheck && treeToCheck.writeable) {
+                            const importClauseNode = TreeUtils.findImportClauseByName(
                               treeToCheck.tree,
-                              a.alias,
+                              moduleNameNode.text,
                             );
-                            if (typeOrTypeAliasCalls) {
-                              references.push(
-                                ...typeOrTypeAliasCalls.map((node) => {
-                                  return { node, uri };
-                                }),
+                            if (importClauseNode) {
+                              const exposedNode = TreeUtils.findExposedTypeOrTypeAliasNode(
+                                importClauseNode,
+                                typeOrTypeAliasNameNode.text,
                               );
+
+                              if (exposedNode) {
+                                references.push({
+                                  node: exposedNode,
+                                  uri,
+                                });
+                              }
                             }
-                          });
+
+                            needsToBeChecked.forEach((a) => {
+                              const typeOrTypeAliasCalls = TreeUtils.findTypeOrTypeAliasCalls(
+                                treeToCheck.tree,
+                                a.alias,
+                              );
+                              if (typeOrTypeAliasCalls) {
+                                references.push(
+                                  ...typeOrTypeAliasCalls.map((node) => {
+                                    return { node, uri };
+                                  }),
+                                );
+                              }
+                            });
+                          }
                         }
                       }
                     }
@@ -443,6 +447,23 @@ export class References {
     return references;
   }
 
+  public static findOperator(node: SyntaxNode): SyntaxNode | undefined {
+    const functionNameNode = TreeUtils.getFunctionNameNodeFromDefinition(node);
+
+    if (functionNameNode) {
+      const infixRef = this.findFunctionCalls(
+        node.tree.rootNode,
+        functionNameNode.text,
+      )?.find(
+        (ref) => ref.parent?.parent?.parent?.type === "infix_declaration",
+      );
+
+      if (infixRef?.parent?.parent?.parent) {
+        return infixRef.parent.parent.parent;
+      }
+    }
+  }
+
   private static findFunctionCalls(
     node: SyntaxNode,
     functionName: string,
@@ -487,12 +508,14 @@ export class References {
     node: SyntaxNode,
   ): SyntaxNode | undefined {
     if (
-      node.previousNamedSibling &&
-      node.previousNamedSibling.type === "type_annotation" &&
-      node.previousNamedSibling.firstChild &&
-      node.previousNamedSibling.firstChild.type === "lower_case_identifier"
+      node.parent &&
+      node.parent.previousNamedSibling &&
+      node.parent.previousNamedSibling.type === "type_annotation" &&
+      node.parent.previousNamedSibling.firstChild &&
+      node.parent.previousNamedSibling.firstChild.type ===
+        "lower_case_identifier"
     ) {
-      return node.previousNamedSibling.firstChild;
+      return node.parent.previousNamedSibling.firstChild;
     }
   }
 }
diff --git a/src/util/sequence.ts b/src/util/sequence.ts
new file mode 100644
index 0000000..0105300
--- /dev/null
+++ b/src/util/sequence.ts
@@ -0,0 +1,54 @@
+export class Sequence<T> {
+  private mappers: ((value: T) => unknown)[] = [];
+
+  constructor(private first: T, private getter: (value: T) => T | undefined) {}
+
+  private iterator = function* <T>(
+    first: T,
+    getter: (value: T) => T | undefined,
+    mappers: ((value: T) => unknown)[],
+  ): Generator<unknown, void, unknown> {
+    const applyMappers = (value: T): unknown =>
+      mappers.reduce((prev, mapper) => mapper(prev) as T, value);
+
+    yield applyMappers(first);
+
+    let next = getter(first);
+    while (next) {
+      yield applyMappers(next);
+      next = getter(next);
+    }
+  };
+
+  public map<U>(mapper: (value: T) => U): Sequence<U> {
+    const newSequence = new Sequence<T>(this.first, this.getter);
+    newSequence.mappers.push(mapper);
+    return (newSequence as unknown) as Sequence<U>;
+  }
+
+  public find(func: (value: T) => boolean): T | undefined {
+    const iterator = this.iterator(this.first, this.getter, this.mappers);
+
+    let next = iterator.next();
+    while (!next.done) {
+      if (func(next.value as T)) {
+        return next.value as T;
+      }
+      next = iterator.next();
+    }
+  }
+
+  public toArray(): T[] {
+    const iterator = this.iterator(this.first, this.getter, this.mappers);
+
+    const array: T[] = [];
+
+    let next = iterator.next();
+    while (!next.done) {
+      array.push(next.value as T);
+      next = iterator.next();
+    }
+
+    return array;
+  }
+}
diff --git a/src/util/treeUtils.ts b/src/util/treeUtils.ts
index 19e847d..081dfc2 100644
--- a/src/util/treeUtils.ts
+++ b/src/util/treeUtils.ts
@@ -16,7 +16,8 @@ export type NodeType =
   | "AnonymousFunctionParameter"
   | "UnionConstructor"
   | "FieldType"
-  | "TypeVariable";
+  | "TypeVariable"
+  | "Port";
 
 const functionNameRegex = new RegExp("[a-zA-Z0-9_]+");
 
@@ -31,7 +32,7 @@ export interface IExposing {
   }[];
 }
 
-function flatMap<T, U>(
+export function flatMap<T, U>(
   array: T[],
   callback: (value: T, index: number, array: T[]) => U[],
 ): U[] {
@@ -91,84 +92,87 @@ export class TreeUtils {
         const exposed: IExposing[] = [];
         if (TreeUtils.findFirstNamedChildOfType("double_dot", exposingList)) {
           if (moduleName) {
-            const functions = TreeUtils.descendantsOfType(
+            TreeUtils.descendantsOfType(
               tree.rootNode,
               "value_declaration",
-            );
-            if (functions) {
-              functions.forEach((elmFunction) => {
-                const declaration = TreeUtils.findFirstNamedChildOfType(
-                  "function_declaration_left",
-                  elmFunction,
-                );
-                if (declaration && declaration.firstNamedChild) {
-                  const functionName = declaration.firstNamedChild.text;
-                  exposed.push({
-                    exposedUnionConstructors: undefined,
-                    name: functionName,
-                    syntaxNode: declaration,
-                    type: "Function",
-                  });
-                }
-              });
-            }
+            )?.forEach((elmFunction) => {
+              const declaration = TreeUtils.findFirstNamedChildOfType(
+                "function_declaration_left",
+                elmFunction,
+              );
+              if (declaration && declaration.firstNamedChild) {
+                const functionName = declaration.firstNamedChild.text;
+                exposed.push({
+                  name: functionName,
+                  syntaxNode: declaration,
+                  type: "Function",
+                });
+              }
+            });
+
+            TreeUtils.findAllNamedChildrenOfType(
+              "port_annotation",
+              tree.rootNode,
+            )?.forEach((elmPort) => {
+              if (elmPort.children[1].text) {
+                exposed.push({
+                  name: elmPort.children[1].text,
+                  syntaxNode: elmPort,
+                  type: "Port",
+                });
+              }
+            });
+
+            this.findAllTypeAliasDeclarations(tree)?.forEach((typeAlias) => {
+              const name = TreeUtils.findFirstNamedChildOfType(
+                "upper_case_identifier",
+                typeAlias,
+              );
+              if (name) {
+                exposed.push({
+                  exposedUnionConstructors: undefined,
+                  name: name.text,
+                  syntaxNode: typeAlias,
+                  type: "TypeAlias",
+                });
+              }
+            });
 
-            const typeAliases = this.findAllTypeAliasDeclarations(tree);
-            if (typeAliases) {
-              typeAliases.forEach((typeAlias) => {
+            this.findAllTypeDeclarations(tree)?.forEach((typeDeclaration) => {
+              const unionConstructors: {
+                name: string;
+                syntaxNode: SyntaxNode;
+                accessibleWithoutPrefix: boolean;
+              }[] = [];
+              TreeUtils.descendantsOfType(
+                typeDeclaration,
+                "union_variant",
+              ).forEach((variant) => {
                 const name = TreeUtils.findFirstNamedChildOfType(
                   "upper_case_identifier",
-                  typeAlias,
+                  variant,
                 );
-                if (name) {
-                  exposed.push({
-                    exposedUnionConstructors: undefined,
+                if (name && name.parent) {
+                  unionConstructors.push({
+                    accessibleWithoutPrefix: false,
                     name: name.text,
-                    syntaxNode: typeAlias,
-                    type: "TypeAlias",
+                    syntaxNode: name.parent,
                   });
                 }
               });
-            }
-
-            const typeDeclarations = this.findAllTypeDeclarations(tree);
-            if (typeDeclarations) {
-              typeDeclarations.forEach((typeDeclaration) => {
-                const unionConstructors: {
-                  name: string;
-                  syntaxNode: SyntaxNode;
-                  accessibleWithoutPrefix: boolean;
-                }[] = [];
-                TreeUtils.descendantsOfType(
-                  typeDeclaration,
-                  "union_variant",
-                ).forEach((variant) => {
-                  const name = TreeUtils.findFirstNamedChildOfType(
-                    "upper_case_identifier",
-                    variant,
-                  );
-                  if (name && name.parent) {
-                    unionConstructors.push({
-                      accessibleWithoutPrefix: false,
-                      name: name.text,
-                      syntaxNode: name.parent,
-                    });
-                  }
+              const typeDeclarationName = TreeUtils.findFirstNamedChildOfType(
+                "upper_case_identifier",
+                typeDeclaration,
+              );
+              if (typeDeclarationName) {
+                exposed.push({
+                  exposedUnionConstructors: unionConstructors,
+                  name: typeDeclarationName.text,
+                  syntaxNode: typeDeclaration,
+                  type: "Type",
                 });
-                const typeDeclarationName = TreeUtils.findFirstNamedChildOfType(
-                  "upper_case_identifier",
-                  typeDeclaration,
-                );
-                if (typeDeclarationName) {
-                  exposed.push({
-                    exposedUnionConstructors: unionConstructors,
-                    name: typeDeclarationName.text,
-                    syntaxNode: typeDeclaration,
-                    type: "Type",
-                  });
-                }
-              });
-            }
+              }
+            });
 
             return { moduleName: moduleName.text, exposing: exposed };
           }
@@ -196,11 +200,14 @@ export class TreeUtils {
             "exposed_value",
           );
 
+          const exposedValuesText = exposedValues.map((a) => a.text);
+
           exposed.push(
-            ...this.findExposedTopLevelFunctions(
-              tree,
-              exposedValues.map((a) => a.text),
-            ),
+            ...this.findExposedTopLevelFunctions(tree, exposedValuesText),
+          );
+
+          exposed.push(
+            ...this.findExposedTopLevelPorts(tree, exposedValuesText),
           );
 
           const exposedTypes = TreeUtils.descendantsOfType(
@@ -444,9 +451,9 @@ export class TreeUtils {
     syntaxNode: SyntaxNode,
     functionName: string,
   ): SyntaxNode | undefined {
-    if (syntaxNode.previousNamedSibling?.type === "let") {
+    if (syntaxNode.parent?.type === "let_in_expr") {
       const foundFunction = this.findFunction(
-        syntaxNode.previousNamedSibling,
+        syntaxNode.parent,
         functionName,
         false,
       );
@@ -455,13 +462,7 @@ export class TreeUtils {
         return foundFunction;
       }
     }
-    if (syntaxNode.parent && syntaxNode.parent.type === "let") {
-      const foundFunction = this.findFunction(syntaxNode, functionName, false);
 
-      if (foundFunction) {
-        return foundFunction;
-      }
-    }
     if (syntaxNode.parent) {
       return this.findLetFunctionNodeDefinition(
         syntaxNode.parent,
@@ -481,15 +482,18 @@ export class TreeUtils {
 
     let ret;
     if (functions) {
-      ret = functions.find((elmFunction) => {
-        const declaration = TreeUtils.findFirstNamedChildOfType(
-          "function_declaration_left",
-          elmFunction,
-        );
-        if (declaration && declaration.firstNamedChild) {
-          return functionName === declaration.firstNamedChild.text;
-        }
-      });
+      ret = functions
+        .map((elmFunction) =>
+          TreeUtils.findFirstNamedChildOfType(
+            "function_declaration_left",
+            elmFunction,
+          ),
+        )
+        .find((declaration) => {
+          if (declaration && declaration.firstNamedChild) {
+            return functionName === declaration.firstNamedChild.text;
+          }
+        });
 
       if (!ret) {
         functions.forEach((elmFunction) => {
@@ -513,6 +517,18 @@ export class TreeUtils {
     }
   }
 
+  public static findPort(tree: Tree, portName: string): SyntaxNode | undefined {
+    return TreeUtils.findAllNamedChildrenOfType(
+      "port_annotation",
+      tree.rootNode,
+    )?.find(
+      (node) =>
+        node.children.length > 1 &&
+        node.children[1].type === "lower_case_identifier" &&
+        node.children[1].text === portName,
+    );
+  }
+
   public static findOperator(
     tree: Tree,
     operatorName: string,
@@ -588,6 +604,17 @@ export class TreeUtils {
     return result.length === 0 ? undefined : result;
   }
 
+  public static findAllTopLevelFunctionDeclarationsWithoutTypeAnnotation(
+    tree: Tree,
+  ): SyntaxNode[] | undefined {
+    const result = tree.rootNode.children.filter(
+      (a) =>
+        a.type === "value_declaration" &&
+        a.previousNamedSibling?.type !== "type_annotation",
+    );
+    return result.length === 0 ? undefined : result;
+  }
+
   public static findAllTypeOrTypeAliasCalls(
     tree: Tree,
   ): SyntaxNode[] | undefined {
@@ -614,10 +641,13 @@ export class TreeUtils {
     if (node.type === "lower_case_identifier") {
       return node;
     }
-    const declaration = TreeUtils.findFirstNamedChildOfType(
-      "function_declaration_left",
-      node,
-    );
+    const declaration =
+      node.type == "function_declaration_left"
+        ? node
+        : TreeUtils.findFirstNamedChildOfType(
+            "function_declaration_left",
+            node,
+          );
     if (declaration && declaration.firstNamedChild) {
       return declaration.firstNamedChild;
     }
@@ -661,7 +691,11 @@ export class TreeUtils {
     nodeAtPosition: SyntaxNode,
   ): { node: SyntaxNode; nodeType: NodeType } | undefined {
     let definitionNode = this.findTypeDeclaration(tree, nodeAtPosition.text);
-    if (definitionNode) {
+    if (
+      definitionNode &&
+      nodeAtPosition.parent?.type !== "value_expr" &&
+      nodeAtPosition.parent?.type !== "union_pattern"
+    ) {
       return { node: definitionNode, nodeType: "Type" };
     }
     definitionNode = this.findTypeAliasDeclaration(tree, nodeAtPosition.text);
@@ -723,7 +757,7 @@ export class TreeUtils {
       const definitionNode =
         nodeAtPosition.parent.parent &&
         nodeAtPosition.parent.parent.parent &&
-        nodeAtPosition.parent.parent.parent.type === "let"
+        nodeAtPosition.parent.parent.parent.type === "let_in_expr"
           ? this.findFunction(
               nodeAtPosition.parent.parent.parent,
               nodeAtPosition.text,
@@ -802,6 +836,20 @@ export class TreeUtils {
           uri: definitionFromOtherFile.fromUri,
         };
       }
+      const portFromOtherFile = this.findImportFromImportList(
+        uri,
+        nodeAtPosition.text,
+        "Port",
+        imports,
+      );
+
+      if (portFromOtherFile) {
+        return {
+          node: portFromOtherFile.node.namedChildren[1],
+          nodeType: "Port",
+          uri: portFromOtherFile.fromUri,
+        };
+      }
     } else if (
       nodeAtPosition.parent &&
       nodeAtPosition.parent.type === "exposed_type" &&
@@ -855,7 +903,11 @@ export class TreeUtils {
       const definitionNode = TreeUtils.findUppercaseQidNode(tree, upperCaseQid);
 
       let definitionFromOtherFile;
-      if (!definitionNode) {
+      if (
+        !definitionNode ||
+        (definitionNode.nodeType === "UnionConstructor" &&
+          upperCaseQid.parent?.type === "type_ref")
+      ) {
         // Make sure the next node is a dot, or else it isn't a Module
         if (TreeUtils.nextNode(nodeAtPosition)?.type === "dot") {
           const endPos =
@@ -926,6 +978,21 @@ export class TreeUtils {
             uri: definitionFromOtherFile.fromUri,
           };
         }
+
+        definitionFromOtherFile = this.findImportFromImportList(
+          uri,
+          TreeUtils.findImportNameNode(tree, upperCaseQid.text)?.text ??
+            upperCaseQid.text,
+          "Module",
+          imports,
+        );
+        if (definitionFromOtherFile) {
+          return {
+            node: definitionFromOtherFile.node,
+            nodeType: "Module",
+            uri: definitionFromOtherFile.fromUri,
+          };
+        }
       }
       if (definitionNode) {
         return {
@@ -984,7 +1051,20 @@ export class TreeUtils {
         };
       }
 
-      const letDefinitionNode = this.findLetFunctionNodeDefinition(
+      const typeVariable = TreeUtils.findTypeAliasTypeVariable(
+        nodeAtPosition,
+        nodeAtPositionText,
+      );
+
+      if (typeVariable) {
+        return {
+          node: typeVariable,
+          nodeType: "TypeVariable",
+          uri,
+        };
+      }
+
+      const letDefinitionNode = TreeUtils.findLetFunctionNodeDefinition(
         nodeAtPosition,
         nodeAtPositionText,
       );
@@ -997,6 +1077,16 @@ export class TreeUtils {
         };
       }
 
+      const portDefinitionNode = TreeUtils.findPort(tree, nodeAtPositionText);
+
+      if (portDefinitionNode) {
+        return {
+          node: portDefinitionNode,
+          nodeType: "Port",
+          uri,
+        };
+      }
+
       const topLevelDefinitionNode = TreeUtils.findFunction(
         tree.rootNode,
         nodeAtPosition.parent.text,
@@ -1030,7 +1120,22 @@ export class TreeUtils {
           };
         }
 
-        const functionDefinitionFromOtherFile = this.findImportFromImportList(
+        const portDefinitionFromOtherFile = TreeUtils.findImportFromImportList(
+          uri,
+          nodeAtPosition.parent.text,
+          "Port",
+          imports,
+        );
+
+        if (portDefinitionFromOtherFile) {
+          return {
+            node: portDefinitionFromOtherFile.node,
+            nodeType: "Port",
+            uri: portDefinitionFromOtherFile.fromUri,
+          };
+        }
+
+        const functionDefinitionFromOtherFile = TreeUtils.findImportFromImportList(
           uri,
           nodeAtPosition.parent.text,
           "Function",
@@ -1075,9 +1180,9 @@ export class TreeUtils {
       if (definitionNode) {
         return { node: definitionNode, uri, nodeType: "Operator" };
       }
-    } else if (nodeAtPosition.parent?.parent?.type === "field_access_expr") {
+    } else if (nodeAtPosition.parent?.type === "field_access_expr") {
       const variableNodes = this.descendantsOfType(
-        nodeAtPosition.parent.parent,
+        nodeAtPosition.parent,
         "lower_case_identifier",
       );
       if (variableNodes.length > 0) {
@@ -1085,24 +1190,28 @@ export class TreeUtils {
           (varNode) => varNode.text === nodeAtPosition.text,
         );
 
-        const variableRef = TreeUtils.findDefinitionNodeByReferencingNode(
-          variableNodes[indexOfNode > 0 ? indexOfNode - 1 : 0],
-          uri,
-          tree,
-          imports,
-        );
+        const variableRef =
+          TreeUtils.findDefinitionNodeByReferencingNode(
+            variableNodes[indexOfNode > 0 ? indexOfNode - 1 : 0],
+            uri,
+            tree,
+            imports,
+          )?.node ?? undefined;
 
         let variableDef: SyntaxNode | null | undefined =
-          TreeUtils.getTypeOrTypeAliasOfFunctionParameter(variableRef?.node) ??
-          TreeUtils.getReturnTypeOrTypeAliasOfFunctionDefinition(
-            variableRef?.node,
-          );
-
-        if (!variableDef && variableRef?.node)
+          TreeUtils.getTypeOrTypeAliasOfFunctionParameter(variableRef) ??
+          (variableRef?.parent
+            ? TreeUtils.getReturnTypeOrTypeAliasOfFunctionDefinition(
+                variableRef.parent,
+              )
+            : undefined);
+
+        if (!variableDef && variableRef) {
           variableDef = TreeUtils.findFirstNamedChildOfType(
             "type_expression",
-            variableRef.node,
+            variableRef,
           )?.firstNamedChild;
+        }
 
         if (variableDef) {
           const variableType = TreeUtils.findDefinitionNodeByReferencingNode(
@@ -1201,7 +1310,8 @@ export class TreeUtils {
       TreeUtils.findParentOfType("type_alias_declaration", nodeAtPosition);
 
     if (parentType?.type === "type_annotation") {
-      const ancestorDeclarations = TreeUtils.getAllAncestorValueDeclarations(
+      const ancestorDeclarations = TreeUtils.getAllAncestorsOfType(
+        "value_declaration",
         parentType,
       );
 
@@ -1254,6 +1364,25 @@ export class TreeUtils {
     }
   }
 
+  public static findTypeAliasTypeVariable(
+    nodeAtPosition: SyntaxNode,
+    nodeAtPositionText: string,
+  ): SyntaxNode | undefined {
+    const parentTypeAlias = this.findParentOfType(
+      "type_alias_declaration",
+      nodeAtPosition,
+    );
+
+    if (parentTypeAlias) {
+      const lowerTypeNames = TreeUtils.findAllNamedChildrenOfType(
+        "lower_type_name",
+        parentTypeAlias,
+      );
+
+      return lowerTypeNames?.find((t) => t.text === nodeAtPositionText);
+    }
+  }
+
   public static findFunctionParameterDefinition(
     node: SyntaxNode,
     functionParameterName: string,
@@ -1269,7 +1398,7 @@ export class TreeUtils {
           const match = this.descendantsOfType(
             node.parent.firstChild,
             "lower_pattern",
-          ).find((a) => a.text === functionParameterName);
+          ).find((a) => a.text === functionParameterName)?.firstNamedChild;
           if (match) {
             return match;
           } else {
@@ -1303,7 +1432,7 @@ export class TreeUtils {
         .find(
           (child) =>
             child.type === "pattern" && child.text === functionParameterName,
-        );
+        )?.firstNamedChild;
 
       if (match) {
         return match;
@@ -1479,13 +1608,15 @@ export class TreeUtils {
       node &&
       node.parent &&
       node.parent.parent &&
-      node.parent.parent.previousNamedSibling &&
-      node.parent.parent.previousNamedSibling.type === "type_annotation" &&
-      node.parent.parent.previousNamedSibling.lastNamedChild
+      node.parent.parent.parent &&
+      node.parent.parent.parent.previousNamedSibling &&
+      node.parent.parent.parent.previousNamedSibling.type ===
+        "type_annotation" &&
+      node.parent.parent.parent.previousNamedSibling.lastNamedChild
     ) {
       const functionParameterNodes = TreeUtils.findAllNamedChildrenOfType(
         ["pattern", "lower_pattern"],
-        node.parent,
+        node.parent.parent,
       );
       if (functionParameterNodes) {
         const matchIndex = functionParameterNodes.findIndex(
@@ -1494,7 +1625,7 @@ export class TreeUtils {
 
         const typeAnnotationNodes = TreeUtils.findAllNamedChildrenOfType(
           ["type_ref", "type_expression"],
-          node.parent.parent.previousNamedSibling.lastNamedChild,
+          node.parent.parent.parent.previousNamedSibling.lastNamedChild,
         );
         if (typeAnnotationNodes) {
           return typeAnnotationNodes[matchIndex];
@@ -1984,16 +2115,17 @@ export class TreeUtils {
   }
 
   /**
-   * This gets a list of all ancestor value declarations
+   * This gets a list of all ancestors of a type
    * in order from the closest declaration up to the top level declaration
    */
-  public static getAllAncestorValueDeclarations(
+  public static getAllAncestorsOfType(
+    type: string,
     node: SyntaxNode,
   ): SyntaxNode[] {
     const declarations = [];
 
     while (node.type !== "file") {
-      if (node.type === "value_declaration") {
+      if (node.type === type) {
         declarations.push(node);
       }
 
@@ -2007,6 +2139,38 @@ export class TreeUtils {
     return declarations;
   }
 
+  public static getQualifierForName(
+    tree: Tree,
+    uri: string,
+    module: string,
+    name: string,
+    imports: IImports,
+  ): string | undefined {
+    if (imports.imports) {
+      const moduleImport = TreeUtils.findImportClauseByName(tree, module);
+
+      if (!moduleImport) {
+        return;
+      }
+
+      if (
+        imports.imports[uri]
+          .filter(
+            (imp) =>
+              imp.fromModuleName === module &&
+              (imp.type === "Type" ||
+                imp.type === "TypeAlias" ||
+                imp.type === "UnionConstructor"),
+          )
+          .some((imp) => imp.alias === name)
+      ) {
+        return "";
+      } else {
+        return `${module}.`;
+      }
+    }
+  }
+
   private static findExposedTopLevelFunctions(
     tree: Tree,
     functionNamesToFind: string[],
@@ -2036,13 +2200,34 @@ export class TreeUtils {
           return {
             exposedUnionConstructors: undefined,
             name: functionNode.text!,
-            syntaxNode: functionNode.node.parent!,
+            syntaxNode: functionNode.node,
             type: "Function",
           };
         },
       );
   }
 
+  private static findExposedTopLevelPorts(
+    tree: Tree,
+    functionNamesToFind: string[],
+  ): IExposing[] {
+    return tree.rootNode.children
+      .filter(
+        (node) =>
+          node.type === "port_annotation" &&
+          node.children.length > 1 &&
+          functionNamesToFind.includes(node.children[1].text),
+      )
+      .map((portNode) => {
+        return {
+          exposedUnionConstructors: undefined,
+          name: portNode.children[1].text,
+          syntaxNode: portNode,
+          type: "Port",
+        };
+      });
+  }
+
   public static findAllImportClauseNodes(tree: Tree): SyntaxNode[] | undefined {
     const result = tree.rootNode.children.filter(
       (a) => a.type === "import_clause",
diff --git a/src/util/types/disjointSet.ts b/src/util/types/disjointSet.ts
new file mode 100644
index 0000000..097a87c
--- /dev/null
+++ b/src/util/types/disjointSet.ts
@@ -0,0 +1,40 @@
+import { Type, TVar } from "./typeInference";
+
+export class DisjointSet {
+  private map: Map<TVar, Type> = new Map<TVar, Type>();
+
+  public set(tvar: TVar, type: Type): void {
+    this.map.set(tvar, type);
+  }
+
+  public get(type: Type): Type | undefined {
+    if (type.nodeType !== "Var") {
+      return type;
+    }
+
+    let node = type;
+    let parent = this.map.get(node);
+
+    while (parent?.nodeType === "Var") {
+      const grandparent = this.map.get(parent);
+
+      if (!grandparent) {
+        return parent;
+      }
+
+      this.map.set(node, grandparent);
+      node = parent;
+      parent = grandparent;
+    }
+
+    return parent ?? node;
+  }
+
+  public contains(tvar: TVar): boolean {
+    return this.map.has(tvar);
+  }
+
+  public toMap(): Map<TVar, Type> {
+    return this.map;
+  }
+}
diff --git a/src/util/types/expressionTree.ts b/src/util/types/expressionTree.ts
new file mode 100644
index 0000000..b941296
--- /dev/null
+++ b/src/util/types/expressionTree.ts
@@ -0,0 +1,659 @@
+import { SyntaxNode } from "web-tree-sitter";
+import { OperatorAssociativity } from "./operatorPrecedence";
+import { TreeUtils } from "../treeUtils";
+import { IImports } from "src/imports";
+import { Utils } from "../utils";
+/* eslint-disable @typescript-eslint/naming-convention */
+
+export type Expression =
+  | EAnonymousFunctionExpr
+  | EAnythingPattern
+  | EBinOpExpr
+  | ECaseOfBranch
+  | ECaseOfExpr
+  | ECharConstant
+  | EConsPattern
+  | EField
+  | EFieldAccessExpr
+  | EFieldAccessorFunctionExpr
+  | EFieldType
+  | EFunctionCallExpr
+  | EFunctionDeclarationLeft
+  | EIfElseExpr
+  | EInfixDeclaration
+  | ELetInExpr
+  | EListExpr
+  | EListPattern
+  | ELowerPattern
+  | ELowerTypeName
+  | ENumberConstant
+  | EOperator
+  | EOperatorAsFunctionExpr
+  | EPattern
+  | EPortAnnotation
+  | ERecordExpr
+  | ERecordType
+  | ERecordPattern
+  | EStringConstant
+  | ETupleExpr
+  | ETuplePattern
+  | ETupleType
+  | ETypeAliasDeclaration
+  | ETypeAnnotation
+  | ETypeDeclaration
+  | ETypeExpression
+  | ETypeRef
+  | ETypeVariable
+  | EUnionPattern
+  | EUnionVariant
+  | EUnitExpr
+  | EValueDeclaration
+  | EValueExpr;
+
+export interface EValueDeclaration extends SyntaxNode {
+  nodeType: "ValueDeclaration";
+  params: string[];
+  body?: Expression;
+  typeAnnotation?: ETypeAnnotation;
+  pattern?: EPattern;
+}
+export interface EFunctionCallExpr extends SyntaxNode {
+  nodeType: "FunctionCallExpr";
+  target: Expression;
+  args: Expression[];
+}
+export interface EIfElseExpr extends SyntaxNode {
+  nodeType: "IfElseExpr";
+  exprList: Expression[];
+}
+export interface ELetInExpr extends SyntaxNode {
+  nodeType: "LetInExpr";
+  valueDeclarations: EValueDeclaration[];
+  body: Expression;
+}
+export interface ECaseOfExpr extends SyntaxNode {
+  nodeType: "CaseOfExpr";
+  expr: Expression;
+  branches: ECaseOfBranch[];
+}
+export interface ECaseOfBranch extends SyntaxNode {
+  nodeType: "CaseOfBranch";
+  pattern: EPattern;
+  expr: Expression;
+}
+export interface EAnonymousFunctionExpr extends SyntaxNode {
+  nodeType: "AnonymousFunctionExpr";
+  params: EPattern[];
+  expr: Expression;
+}
+export interface EUnionVariant extends SyntaxNode {
+  nodeType: "UnionVariant";
+  name: string;
+  params: Expression[];
+}
+export interface EUnionPattern extends SyntaxNode {
+  nodeType: "UnionPattern";
+  constructor: SyntaxNode;
+  namedParams: Expression[];
+  argPatterns: Expression[];
+}
+export interface EValueExpr extends SyntaxNode {
+  nodeType: "ValueExpr";
+  name: string;
+}
+export interface EBinOpExpr extends SyntaxNode {
+  nodeType: "BinOpExpr";
+  parts: Expression[];
+}
+export interface EOperator extends SyntaxNode {
+  nodeType: "Operator";
+}
+export interface EOperatorAsFunctionExpr extends SyntaxNode {
+  nodeType: "OperatorAsFunctionExpr";
+}
+export interface ENumberConstant extends SyntaxNode {
+  nodeType: "NumberConstant";
+  isFloat: boolean;
+}
+interface EStringConstant extends SyntaxNode {
+  nodeType: "StringConstant";
+}
+export interface ETypeExpression extends SyntaxNode {
+  nodeType: "TypeExpression";
+  segments: Expression[];
+}
+export interface ETypeRef extends SyntaxNode {
+  nodeType: "TypeRef";
+}
+export interface ETypeDeclaration extends SyntaxNode {
+  nodeType: "TypeDeclaration";
+  name: string;
+  moduleName: string;
+  unionVariants: Expression[];
+  typeNames: Expression[];
+}
+export interface ETypeVariable extends SyntaxNode {
+  nodeType: "TypeVariable";
+}
+export interface ETypeAnnotation extends SyntaxNode {
+  nodeType: "TypeAnnotation";
+  name: string;
+  typeExpression?: ETypeExpression;
+}
+export interface EInfixDeclaration extends SyntaxNode {
+  nodeType: "InfixDeclaration";
+  precedence: number;
+  associativity: OperatorAssociativity;
+}
+export interface EFunctionDeclarationLeft extends SyntaxNode {
+  nodeType: "FunctionDeclarationLeft";
+  params: Pattern[];
+}
+type Pattern = EPattern | ELowerPattern;
+export interface EPattern extends SyntaxNode {
+  nodeType: "Pattern";
+  patternAs?: ELowerPattern;
+}
+export interface ELowerPattern extends SyntaxNode {
+  nodeType: "LowerPattern";
+}
+export interface ELowerTypeName extends SyntaxNode {
+  nodeType: "LowerTypeName";
+}
+export interface EUnitExpr extends SyntaxNode {
+  nodeType: "UnitExpr";
+}
+export interface ETupleExpr extends SyntaxNode {
+  nodeType: "TupleExpr";
+  exprList: Expression[];
+}
+export interface EAnythingPattern extends SyntaxNode {
+  nodeType: "AnythingPattern";
+}
+export interface ETuplePattern extends SyntaxNode {
+  nodeType: "TuplePattern";
+  patterns: EPattern[];
+}
+export interface ETupleType extends SyntaxNode {
+  nodeType: "TupleType";
+  typeExpressions: ETypeExpression[];
+  unitExpr?: EUnitExpr;
+}
+export interface EListExpr extends SyntaxNode {
+  nodeType: "ListExpr";
+  exprList: Expression[];
+}
+export interface EListPattern extends SyntaxNode {
+  nodeType: "ListPattern";
+  parts: Expression[];
+}
+export interface EConsPattern extends SyntaxNode {
+  nodeType: "ConsPattern";
+  parts: Expression[];
+}
+export interface EFieldType extends SyntaxNode {
+  nodeType: "FieldType";
+  name: string;
+  typeExpression: ETypeExpression;
+}
+export interface ERecordType extends SyntaxNode {
+  nodeType: "RecordType";
+  baseType: Expression;
+  fieldTypes: EFieldType[];
+}
+export interface ETypeAliasDeclaration extends SyntaxNode {
+  nodeType: "TypeAliasDeclaration";
+  name: SyntaxNode;
+  typeVariables: Expression[];
+  typeExpression: ETypeExpression;
+}
+export interface EField extends SyntaxNode {
+  nodeType: "Field";
+  name: Expression;
+  expression: Expression;
+}
+export interface EFieldAccessExpr extends SyntaxNode {
+  nodeType: "FieldAccessExpr";
+  target: Expression;
+}
+export interface EFieldAccessorFunctionExpr extends SyntaxNode {
+  nodeType: "FieldAccessorFunctionExpr";
+}
+export interface ERecordPattern extends SyntaxNode {
+  nodeType: "RecordPattern";
+  patternList: ELowerPattern[];
+}
+export interface ERecordExpr extends SyntaxNode {
+  nodeType: "RecordExpr";
+  baseRecord: SyntaxNode;
+  fields: EField[];
+}
+export interface EPortAnnotation extends SyntaxNode {
+  nodeType: "PortAnnotation";
+  name: string;
+  typeExpression: ETypeExpression;
+}
+export interface ECharConstant extends SyntaxNode {
+  nodeType: "CharConstant";
+}
+
+export function mapSyntaxNodeToExpression(
+  node: SyntaxNode | null | undefined,
+): Expression | undefined {
+  if (!node) return;
+
+  switch (node.type) {
+    case "lower_case_identifier":
+      return node as Expression;
+    case "value_declaration":
+      {
+        const body = mapSyntaxNodeToExpression(
+          node.namedChildren[node.namedChildren.length - 1],
+        );
+
+        const typeAnnotation = mapSyntaxNodeToExpression(
+          TreeUtils.getTypeAnnotation(node),
+        );
+
+        if (body) {
+          const params =
+            node.firstNamedChild?.namedChildren.slice(1).map((a) => a.text) ??
+            [];
+          return Object.assign(node, {
+            nodeType: "ValueDeclaration",
+            params,
+            body,
+            typeAnnotation,
+            pattern: mapSyntaxNodeToExpression(
+              node.childForFieldName("pattern"),
+            ),
+          } as EValueDeclaration);
+        }
+      }
+      break;
+    case "value_expr":
+      return Object.assign(node, {
+        nodeType: "ValueExpr",
+        name: node.text,
+      } as EValueExpr);
+
+    case "bin_op_expr": {
+      return Object.assign(node, {
+        nodeType: "BinOpExpr",
+        parts: node.children
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EBinOpExpr);
+    }
+
+    case "operator_identifier": {
+      return Object.assign(node, {
+        nodeType: "Operator",
+      } as EOperator);
+    }
+
+    case "number_constant_expr":
+      return Object.assign(node, {
+        nodeType: "NumberConstant",
+        isFloat: false,
+      } as ENumberConstant);
+
+    case "string_constant_expr":
+      return Object.assign(node, {
+        nodeType: "StringConstant",
+      } as EStringConstant);
+
+    case "parenthesized_expr":
+      return mapSyntaxNodeToExpression(node.children[1]);
+
+    case "function_call_expr":
+      {
+        const target = mapSyntaxNodeToExpression(node.firstNamedChild);
+
+        if (target) {
+          return Object.assign(node, {
+            nodeType: "FunctionCallExpr",
+            target,
+            args: node.children
+              .slice(1)
+              .map(mapSyntaxNodeToExpression)
+              .filter(Utils.notUndefined),
+          } as EFunctionCallExpr);
+        }
+      }
+      break;
+
+    case "type_annotation":
+      return Object.assign(node, {
+        nodeType: "TypeAnnotation",
+        name: node.firstNamedChild?.text ?? "",
+        typeExpression: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("type_expression", node),
+        ),
+      } as ETypeAnnotation);
+
+    case "type_expression":
+      return Object.assign(node, {
+        nodeType: "TypeExpression",
+        segments: node.children
+          .filter(
+            (n) =>
+              n.type !== "arrow" &&
+              n.type !== "left_parenthesis" &&
+              n.type !== "right_parenthesis",
+          )
+          .map(mapSyntaxNodeToExpression),
+      } as ETypeExpression);
+
+    case "type_variable":
+      return Object.assign(node, { nodeType: "TypeVariable" } as ETypeVariable);
+
+    case "type_ref":
+      return Object.assign(node, { nodeType: "TypeRef" } as ETypeRef);
+
+    case "type_declaration":
+      return Object.assign(node, {
+        nodeType: "TypeDeclaration",
+        name:
+          TreeUtils.findFirstNamedChildOfType("upper_case_identifier", node)
+            ?.text ?? "",
+        moduleName: TreeUtils.getModuleNameNode(node.tree)?.text ?? "",
+        unionVariants: TreeUtils.findAllNamedChildrenOfType(
+          "union_variant",
+          node,
+        ),
+        typeNames:
+          TreeUtils.findAllNamedChildrenOfType("lower_type_name", node) ?? [],
+      } as ETypeDeclaration);
+
+    case "infix_declaration":
+      return Object.assign(node, {
+        nodeType: "InfixDeclaration",
+        precedence: parseInt(
+          TreeUtils.findFirstNamedChildOfType("number_literal", node)?.text ??
+            "",
+        ),
+        associativity:
+          TreeUtils.findFirstNamedChildOfType(
+            "lower_case_identifier",
+            node,
+          )?.text.toUpperCase() ?? "NON",
+      } as EInfixDeclaration);
+
+    case "function_declaration_left":
+      return Object.assign(node, {
+        nodeType: "FunctionDeclarationLeft",
+        params: node.namedChildren
+          .filter((n) => n.type.includes("pattern"))
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EFunctionDeclarationLeft);
+
+    case "pattern": {
+      const asNode = TreeUtils.findFirstNamedChildOfType("as", node);
+      return Object.assign(node, {
+        nodeType: "Pattern",
+        patternAs: mapSyntaxNodeToExpression(asNode?.nextNamedSibling),
+      } as EPattern);
+    }
+
+    case "lower_pattern":
+      return Object.assign(node, {
+        nodeType: "LowerPattern",
+      } as ELowerPattern);
+
+    case "lower_type_name":
+      return Object.assign(node, {
+        nodeType: "LowerTypeName",
+      } as ELowerTypeName);
+
+    case "union_variant":
+      return Object.assign(node, {
+        nodeType: "UnionVariant",
+        name: node.firstNamedChild?.text ?? "",
+        params: node.namedChildren
+          .slice(1)
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EUnionVariant);
+
+    case "if_else_expr":
+      return Object.assign(node, {
+        nodeType: "IfElseExpr",
+        exprList: node.namedChildren.map((n) => mapSyntaxNodeToExpression(n)),
+      } as EIfElseExpr);
+
+    case "let_in_expr":
+      return Object.assign(node, {
+        nodeType: "LetInExpr",
+        valueDeclarations:
+          TreeUtils.findAllNamedChildrenOfType("value_declaration", node)?.map(
+            mapSyntaxNodeToExpression,
+          ) ?? [],
+        body: mapSyntaxNodeToExpression(node.lastNamedChild),
+      } as ELetInExpr);
+
+    case "case_of_expr":
+      return Object.assign(node, {
+        nodeType: "CaseOfExpr",
+        expr: mapSyntaxNodeToExpression(node.namedChildren[1]),
+        branches: node.namedChildren
+          .slice(3)
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as ECaseOfExpr);
+
+    case "case_of_branch":
+      return Object.assign(node, {
+        nodeType: "CaseOfBranch",
+        pattern: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("pattern", node),
+        ),
+        expr: mapSyntaxNodeToExpression(node.lastNamedChild),
+      } as ECaseOfBranch);
+
+    case "anonymous_function_expr":
+      return Object.assign(node, {
+        nodeType: "AnonymousFunctionExpr",
+        params: TreeUtils.findAllNamedChildrenOfType("pattern", node)
+          ?.map(mapSyntaxNodeToExpression)
+          .filter((n) => n?.nodeType === "Pattern"),
+        expr: mapSyntaxNodeToExpression(node.lastNamedChild),
+      } as EAnonymousFunctionExpr);
+
+    case "unit_expr":
+      return Object.assign(node, {
+        nodeType: "UnitExpr",
+      } as EUnitExpr);
+
+    case "tuple_expr":
+      return Object.assign(node, {
+        nodeType: "TupleExpr",
+        exprList: node.namedChildren
+          .filter(
+            (n) =>
+              n.type !== "left_parenthesis" &&
+              n.type !== "comma" &&
+              n.type !== "right_parenthesis",
+          )
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as ETupleExpr);
+
+    case "anything_pattern":
+      return Object.assign(node, {
+        nodeType: "AnythingPattern",
+      } as EAnythingPattern);
+
+    case "tuple_pattern":
+      return Object.assign(node, {
+        nodeType: "TuplePattern",
+        patterns: TreeUtils.findAllNamedChildrenOfType("pattern", node)
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as ETuplePattern);
+
+    case "tuple_type":
+      return Object.assign(node, {
+        nodeType: "TupleType",
+        typeExpressions: TreeUtils.findAllNamedChildrenOfType(
+          "type_expression",
+          node,
+        )
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+        unitExpr: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("unit_expr", node),
+        ),
+      } as ETupleType);
+
+    case "list_expr":
+      return Object.assign(node, {
+        nodeType: "ListExpr",
+        exprList: node.namedChildren
+          .filter((n) => n.type.endsWith("expr"))
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EListExpr);
+
+    case "list_pattern":
+      return Object.assign(node, {
+        nodeType: "ListPattern",
+        parts: node.namedChildren
+          .filter((n) => n.type.includes("pattern"))
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EListPattern);
+
+    case "union_pattern":
+      return Object.assign(node, {
+        nodeType: "UnionPattern",
+        constructor: node.firstNamedChild,
+        namedParams: node.namedChildren
+          .filter((n) => n.type.includes("pattern"))
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+        argPatterns: node.namedChildren
+          .filter((n) => n.type.includes("pattern"))
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EUnionPattern);
+
+    case "cons_pattern":
+      return Object.assign(node, {
+        nodeType: "ConsPattern",
+        parts: node.namedChildren
+          .filter((n) => n.type.includes("pattern"))
+          .map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as EConsPattern);
+    case "record_type":
+      return Object.assign(node, {
+        nodeType: "RecordType",
+        baseType: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("record_base_identifier", node),
+        ),
+        fieldTypes: TreeUtils.findAllNamedChildrenOfType("field_type", node)
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as ERecordType);
+    case "field_type":
+      return Object.assign(node, {
+        nodeType: "FieldType",
+        name:
+          TreeUtils.findFirstNamedChildOfType("lower_case_identifier", node)
+            ?.text ?? "",
+        typeExpression: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("type_expression", node),
+        ),
+      } as EFieldType);
+    case "type_alias_declaration":
+      return Object.assign(node, {
+        nodeType: "TypeAliasDeclaration",
+        name: TreeUtils.findFirstNamedChildOfType(
+          "upper_case_identifier",
+          node,
+        ),
+        typeVariables:
+          TreeUtils.findAllNamedChildrenOfType("lower_type_name", node)
+            ?.map(mapSyntaxNodeToExpression)
+            .filter(Utils.notUndefined) ?? [],
+        typeExpression: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("type_expression", node),
+        ),
+      } as ETypeAliasDeclaration);
+    case "field":
+      return Object.assign(node, {
+        nodeType: "Field",
+        name: node.firstNamedChild,
+        expression: mapSyntaxNodeToExpression(node.lastNamedChild),
+      } as EField);
+    case "field_access_expr":
+      return Object.assign(node, {
+        nodeType: "FieldAccessExpr",
+        target: mapSyntaxNodeToExpression(node.firstNamedChild),
+      } as EFieldAccessExpr);
+    case "field_accessor_function_expr":
+      return Object.assign(node, {
+        nodeType: "FieldAccessorFunctionExpr",
+      } as EFieldAccessorFunctionExpr);
+    case "record_pattern":
+      return Object.assign(node, {
+        nodeType: "RecordPattern",
+        patternList: TreeUtils.findAllNamedChildrenOfType("lower_pattern", node)
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as ERecordPattern);
+    case "record_expr":
+      return Object.assign(node, {
+        nodeType: "RecordExpr",
+        baseRecord: TreeUtils.findFirstNamedChildOfType(
+          "record_base_identifier",
+          node,
+        ),
+        fields: TreeUtils.findAllNamedChildrenOfType("field", node)
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined),
+      } as ERecordExpr);
+    case "port_annotation":
+      return Object.assign(node, {
+        nodeType: "PortAnnotation",
+        name: node.children[1]?.text ?? "",
+        typeExpression: mapSyntaxNodeToExpression(
+          TreeUtils.findFirstNamedChildOfType("type_expression", node),
+        ),
+      } as EPortAnnotation);
+    case "char_constant":
+      return Object.assign(node, {
+        nodeType: "CharConstant",
+      } as ECharConstant);
+    default:
+      return mapSyntaxNodeToExpression(node.firstNamedChild);
+  }
+}
+
+export function findDefinition(
+  e: SyntaxNode | undefined | null,
+  uri: string,
+  imports: IImports,
+): { expr: Expression; uri: string } | undefined {
+  if (!e) {
+    return;
+  }
+
+  const definition = TreeUtils.findDefinitionNodeByReferencingNode(
+    e,
+    uri,
+    e.tree,
+    imports,
+  );
+
+  const mappedNode = mapSyntaxNodeToExpression(definition?.node);
+
+  if (mappedNode && definition) {
+    return {
+      expr: mappedNode,
+      uri: definition.uri,
+    };
+  }
+}
diff --git a/src/util/types/operatorPrecedence.ts b/src/util/types/operatorPrecedence.ts
new file mode 100644
index 0000000..ffd94bd
--- /dev/null
+++ b/src/util/types/operatorPrecedence.ts
@@ -0,0 +1,103 @@
+import { Expression } from "./expressionTree";
+import { SyntaxNodeMap } from "./syntaxNodeMap";
+
+export type OperatorAssociativity = "LEFT" | "RIGHT" | "NON";
+
+export interface IOperatorPrecedence {
+  precedence: number;
+  associativity: OperatorAssociativity;
+}
+
+export class BinaryExprTree {
+  private static DEFAULT_PRECEDENCE = -1;
+
+  public type: "Operand" | "Binary";
+
+  constructor(type: "Operand" | "Binary") {
+    this.type = type;
+  }
+
+  public static parse(
+    expression: Expression[],
+    operatorPrecedences: SyntaxNodeMap<Expression, IOperatorPrecedence>,
+  ): BinaryExprTree {
+    return BinaryExprTree.parseExpression(
+      expression,
+      operatorPrecedences,
+      BinaryExprTree.DEFAULT_PRECEDENCE,
+      0,
+    )[0];
+  }
+
+  private static parseExpression(
+    expression: Expression[],
+    operatorPrecedences: SyntaxNodeMap<Expression, IOperatorPrecedence>,
+    precedence: number,
+    index: number,
+  ): [BinaryExprTree, number] {
+    let left: BinaryExprTree = new Operand(expression[index]);
+
+    if (index >= expression.length - 1) {
+      return [left, index + 1];
+    }
+
+    let i = index + 1;
+
+    function nextPrecendence(): number {
+      if (i >= expression.length - 1) {
+        return BinaryExprTree.DEFAULT_PRECEDENCE;
+      } else {
+        return (
+          operatorPrecedences.get(expression[i])?.precedence ??
+          BinaryExprTree.DEFAULT_PRECEDENCE
+        );
+      }
+    }
+
+    while (precedence < nextPrecendence()) {
+      const operator = expression[i];
+      const funcPrecedence = operatorPrecedences.get(operator);
+      const rightPrecedence =
+        funcPrecedence?.associativity === "RIGHT"
+          ? funcPrecedence.precedence - 1
+          : funcPrecedence?.precedence ?? BinaryExprTree.DEFAULT_PRECEDENCE;
+
+      const result = BinaryExprTree.parseExpression(
+        expression,
+        operatorPrecedences,
+        rightPrecedence,
+        i + 1,
+      );
+      left = new Binary(left, operator, result[0]);
+      i = result[1];
+    }
+
+    return [left, i];
+  }
+}
+
+export class Operand extends BinaryExprTree {
+  public operand: Expression;
+
+  constructor(operand: Expression) {
+    super("Operand");
+    this.operand = operand;
+  }
+}
+
+export class Binary extends BinaryExprTree {
+  public left: BinaryExprTree;
+  public operator: Expression;
+  public right: BinaryExprTree;
+
+  constructor(
+    left: BinaryExprTree,
+    operator: Expression,
+    right: BinaryExprTree,
+  ) {
+    super("Binary");
+    this.left = left;
+    this.operator = operator;
+    this.right = right;
+  }
+}
diff --git a/src/util/types/recordFieldReferenceTable.ts b/src/util/types/recordFieldReferenceTable.ts
new file mode 100644
index 0000000..cf72f66
--- /dev/null
+++ b/src/util/types/recordFieldReferenceTable.ts
@@ -0,0 +1,75 @@
+import { SyntaxNode } from "web-tree-sitter";
+import { EFieldType } from "./expressionTree";
+
+export class RecordFieldReferenceTable {
+  private refsByField: Map<string, SyntaxNode[]>;
+  private _frozen = false;
+
+  public get frozen(): boolean {
+    return this._frozen;
+  }
+
+  constructor(refsByField?: Map<string, SyntaxNode[]>) {
+    this.refsByField = refsByField ?? new Map<string, SyntaxNode[]>();
+  }
+
+  public static fromExpressions(
+    fieldExpressions: EFieldType[],
+  ): RecordFieldReferenceTable {
+    const fieldRefs: Map<string, SyntaxNode[]> = new Map<
+      string,
+      SyntaxNode[]
+    >();
+
+    fieldExpressions.forEach((field) => {
+      fieldRefs.set(field.name, [field]);
+    });
+
+    return new RecordFieldReferenceTable(fieldRefs);
+  }
+
+  public get(field: string): SyntaxNode[] {
+    return this.refsByField.get(field) ?? [];
+  }
+
+  public addAll(other: RecordFieldReferenceTable): void {
+    if (this._frozen || other.refsByField === this.refsByField) {
+      return;
+    }
+
+    other.refsByField.forEach((refs, field) => {
+      let set = this.refsByField.get(field);
+
+      if (!set) {
+        this.refsByField.set(field, []);
+        set = this.refsByField.get(field) ?? [];
+      }
+
+      set.push(...refs);
+    });
+  }
+
+  public plus(other: RecordFieldReferenceTable): RecordFieldReferenceTable {
+    const newRefs = new Map<string, SyntaxNode[]>();
+
+    this.refsByField.forEach((set, field) => {
+      const otherSet = other.refsByField.get(field) ?? [];
+      newRefs.set(field, [...set, ...otherSet]);
+    });
+    other.refsByField.forEach((set, field) => {
+      if (!newRefs.get(field)) {
+        newRefs.set(field, set);
+      }
+    });
+
+    return new RecordFieldReferenceTable(newRefs);
+  }
+
+  public isEmpty(): boolean {
+    return this.refsByField.size === 0;
+  }
+
+  public freeze(): void {
+    this._frozen = true;
+  }
+}
diff --git a/src/util/types/syntaxNodeMap.ts b/src/util/types/syntaxNodeMap.ts
new file mode 100644
index 0000000..424df99
--- /dev/null
+++ b/src/util/types/syntaxNodeMap.ts
@@ -0,0 +1,55 @@
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
+import { SyntaxNode } from "web-tree-sitter";
+
+export class SyntaxNodeMap<K extends SyntaxNode, V> {
+  private map: Map<number, V> = new Map<number, V>();
+
+  public set(key: K, value: V): void {
+    if (!("id" in key)) {
+      throw new Error("SyntaxNodeMap key must have an `id` property");
+    }
+
+    this.map.set((<any>key).id, value);
+  }
+
+  public get(key: K): V | undefined {
+    if (!("id" in key)) {
+      throw new Error("SyntaxNodeMap key must have an `id` property");
+    }
+
+    return this.map.get((<any>key).id);
+  }
+
+  public getOrSet(key: K, setter: () => V): V {
+    const existing = this.get(key);
+
+    if (existing) {
+      return existing;
+    }
+
+    const result = setter();
+
+    this.set(key, result);
+    return result;
+  }
+
+  public has(key: K): boolean {
+    if (!("id" in key)) {
+      throw new Error("SyntaxNodeMap key must have an `id` property");
+    }
+
+    return this.map.has((<any>key).id);
+  }
+
+  public mapValues(callback: (value: V) => V): void {
+    this.map.forEach((val, key) => {
+      this.map.set(key, callback(val));
+    });
+  }
+
+  public forEach(callback: (val: V, key: K) => void): void {
+    this.map.forEach((val, key) =>
+      callback(val, ({ id: key } as unknown) as K),
+    );
+  }
+}
diff --git a/src/util/types/typeCache.ts b/src/util/types/typeCache.ts
new file mode 100644
index 0000000..56adcd6
--- /dev/null
+++ b/src/util/types/typeCache.ts
@@ -0,0 +1,41 @@
+import { SyntaxNode } from "web-tree-sitter";
+import { SyntaxNodeMap } from "./syntaxNodeMap";
+import { InferenceResult } from "./typeInference";
+
+type CacheKey =
+  | "PACKAGE_TYPE_ANNOTATION"
+  | "PACKAGE_TYPE_AND_TYPE_ALIAS"
+  | "PACKAGE_VALUE";
+
+export class TypeCache {
+  private packageTypeAnnotation: SyntaxNodeMap<SyntaxNode, InferenceResult>;
+  private packageTypeAndTypeAlias: SyntaxNodeMap<SyntaxNode, InferenceResult>;
+  private packageValue: SyntaxNodeMap<SyntaxNode, InferenceResult>;
+
+  constructor() {
+    this.packageTypeAnnotation = new SyntaxNodeMap<
+      SyntaxNode,
+      InferenceResult
+    >();
+    this.packageTypeAndTypeAlias = new SyntaxNodeMap<
+      SyntaxNode,
+      InferenceResult
+    >();
+    this.packageValue = new SyntaxNodeMap<SyntaxNode, InferenceResult>();
+  }
+
+  public getOrSet(
+    key: CacheKey,
+    node: SyntaxNode,
+    setter: () => InferenceResult,
+  ): InferenceResult {
+    switch (key) {
+      case "PACKAGE_TYPE_ANNOTATION":
+        return this.packageTypeAnnotation.getOrSet(node, setter);
+      case "PACKAGE_TYPE_AND_TYPE_ALIAS":
+        return this.packageTypeAndTypeAlias.getOrSet(node, setter);
+      case "PACKAGE_VALUE":
+        return this.packageValue.getOrSet(node, setter);
+    }
+  }
+}
diff --git a/src/util/types/typeExpression.ts b/src/util/types/typeExpression.ts
new file mode 100644
index 0000000..aad0e43
--- /dev/null
+++ b/src/util/types/typeExpression.ts
@@ -0,0 +1,499 @@
+import {
+  Type,
+  TVar,
+  uncurryFunction,
+  InferenceResult,
+  Diagnostic,
+  TUnit,
+  TUnknown,
+  TFunction,
+  TTuple,
+  TUnion,
+  TList,
+  TRecord,
+  Alias,
+  typeArgumentCountError,
+} from "./typeInference";
+import {
+  Expression,
+  ETypeExpression,
+  ETypeDeclaration,
+  ETypeRef,
+  ETypeVariable,
+  mapSyntaxNodeToExpression,
+  ETypeAnnotation,
+  findDefinition,
+  EUnionVariant,
+  ERecordType,
+  ETypeAliasDeclaration,
+  EPortAnnotation,
+} from "./expressionTree";
+import { TreeUtils } from "../treeUtils";
+import { TypeReplacement } from "./typeReplacement";
+import { SyntaxNodeMap } from "./syntaxNodeMap";
+import { Utils } from "../utils";
+import { RecordFieldReferenceTable } from "./recordFieldReferenceTable";
+import { IElmWorkspace } from "src/elmWorkspace";
+
+export class TypeExpression {
+  // All the type variables we've seen
+  private varsByExpression: SyntaxNodeMap<Expression, TVar> = new SyntaxNodeMap<
+    Expression,
+    TVar
+  >();
+
+  private expressionTypes: SyntaxNodeMap<Expression, Type> = new SyntaxNodeMap<
+    Expression,
+    Type
+  >();
+
+  private diagnostics: Diagnostic[] = [];
+
+  constructor(
+    private root: Expression,
+    private uri: string,
+    private workspace: IElmWorkspace,
+    private rigidVars: boolean,
+    private activeAliases: Set<ETypeAliasDeclaration> = new Set(),
+  ) {}
+
+  public static typeDeclarationInference(
+    e: ETypeDeclaration,
+    uri: string,
+    workspace: IElmWorkspace,
+  ): InferenceResult {
+    const setter = (): InferenceResult => {
+      const inferenceResult = new TypeExpression(
+        e,
+        uri,
+        workspace,
+        false,
+      ).inferTypeDeclaration(e);
+
+      TypeReplacement.freeze(inferenceResult.type);
+
+      return {
+        ...inferenceResult,
+        type: TypeReplacement.freshenVars(inferenceResult.type),
+      };
+    };
+
+    if (!workspace.getForest().getByUri(uri)?.writeable) {
+      return workspace
+        .getTypeCache()
+        .getOrSet("PACKAGE_TYPE_AND_TYPE_ALIAS", e, setter);
+    } else {
+      return setter();
+    }
+  }
+
+  public static typeAliasDeclarationInference(
+    e: ETypeAliasDeclaration,
+    uri: string,
+    workspace: IElmWorkspace,
+    activeAliases = new Set<ETypeAliasDeclaration>(),
+  ): InferenceResult {
+    const setter = (): InferenceResult => {
+      const inferenceResult = new TypeExpression(
+        e,
+        uri,
+        workspace,
+        false,
+        activeAliases,
+      ).inferTypeAliasDeclaration(e);
+
+      TypeReplacement.freeze(inferenceResult.type);
+
+      return {
+        ...inferenceResult,
+        type: TypeReplacement.freshenVars(inferenceResult.type),
+      };
+    };
+
+    if (!workspace.getForest().getByUri(uri)?.writeable) {
+      return workspace
+        .getTypeCache()
+        .getOrSet("PACKAGE_TYPE_AND_TYPE_ALIAS", e, setter);
+    } else {
+      return setter();
+    }
+  }
+
+  public static typeAnnotationInference(
+    e: ETypeAnnotation,
+    uri: string,
+    workspace: IElmWorkspace,
+    rigid = true,
+  ): InferenceResult | undefined {
+    if (!e.typeExpression) {
+      return;
+    }
+
+    const setter = (): InferenceResult => {
+      const inferenceResult = new TypeExpression(
+        e,
+        uri,
+        workspace,
+        true,
+      ).inferTypeExpression(e.typeExpression!);
+
+      let type = TypeReplacement.replace(inferenceResult.type, new Map());
+      TypeReplacement.freeze(inferenceResult.type);
+
+      if (!rigid) {
+        type = TypeReplacement.flexify(type);
+      }
+
+      return { ...inferenceResult, type };
+    };
+
+    if (!workspace.getForest().getByUri(uri)?.writeable) {
+      return workspace
+        .getTypeCache()
+        .getOrSet("PACKAGE_TYPE_ANNOTATION", e.typeExpression, setter);
+    } else {
+      return setter();
+    }
+  }
+
+  public static unionVariantInference(
+    e: EUnionVariant,
+    uri: string,
+    workspace: IElmWorkspace,
+  ): InferenceResult {
+    const inferenceResult = new TypeExpression(
+      e,
+      uri,
+      workspace,
+      false,
+    ).inferUnionConstructor(e);
+    TypeReplacement.freeze(inferenceResult.type);
+
+    return {
+      ...inferenceResult,
+      type: TypeReplacement.freshenVars(inferenceResult.type),
+    };
+  }
+
+  public static portAnnotationInference(
+    e: EPortAnnotation,
+    uri: string,
+    workspace: IElmWorkspace,
+  ): InferenceResult {
+    const inferenceResult = new TypeExpression(
+      e,
+      uri,
+      workspace,
+      false,
+    ).inferPortAnnotation(e);
+    TypeReplacement.freeze(inferenceResult.type);
+
+    return {
+      ...inferenceResult,
+      type: TypeReplacement.freshenVars(inferenceResult.type),
+    };
+  }
+
+  private inferTypeDeclaration(
+    typeDeclaration: ETypeDeclaration,
+  ): InferenceResult {
+    return this.toResult(this.typeDeclarationType(typeDeclaration));
+  }
+
+  private inferTypeExpression(typeExpr: ETypeExpression): InferenceResult {
+    return this.toResult(this.typeExpressionType(typeExpr));
+  }
+
+  private inferUnionConstructor(unionVariant: EUnionVariant): InferenceResult {
+    const declaration = mapSyntaxNodeToExpression(
+      TreeUtils.findParentOfType("type_declaration", unionVariant),
+    );
+
+    const declarationType: Type =
+      declaration && declaration.nodeType === "TypeDeclaration"
+        ? this.typeDeclarationType(declaration)
+        : TUnknown;
+
+    const params = unionVariant.params.map((t) =>
+      this.typeSignatureSegmentType(t),
+    );
+
+    const type: Type =
+      params.length > 0 ? TFunction(params, declarationType) : declarationType;
+
+    return this.toResult(type);
+  }
+
+  private inferPortAnnotation(
+    portAnnotation: EPortAnnotation,
+  ): InferenceResult {
+    const type = this.typeExpressionType
+      ? this.typeExpressionType(portAnnotation.typeExpression)
+      : TUnknown;
+    return this.toResult(type);
+  }
+
+  private inferTypeAliasDeclaration(
+    declaration: ETypeAliasDeclaration,
+  ): InferenceResult {
+    if (this.activeAliases.has(declaration)) {
+      this.diagnostics.push({
+        node: declaration,
+        endNode: declaration,
+        message: "BadRecursionError",
+      });
+      return this.toResult(TUnknown);
+    }
+
+    this.activeAliases.add(declaration);
+
+    const type = declaration.typeExpression
+      ? this.typeExpressionType(declaration.typeExpression)
+      : TUnknown;
+
+    const params = declaration.typeVariables.map(this.getTypeVar.bind(this));
+    const moduleName =
+      TreeUtils.getModuleNameNode(declaration.tree)?.text ?? "";
+    const alias: Alias = {
+      module: moduleName,
+      name: declaration.name.text,
+      parameters: params,
+    };
+    return this.toResult({ ...type, alias: alias });
+  }
+
+  private typeExpressionType(typeExpr: ETypeExpression): Type {
+    const segmentTypes = typeExpr.segments.map((s) =>
+      this.typeSignatureSegmentType(s),
+    );
+    if (segmentTypes.length === 1) {
+      return segmentTypes[0];
+    } else {
+      return uncurryFunction(
+        TFunction(
+          segmentTypes.slice(0, segmentTypes.length - 1),
+          segmentTypes[segmentTypes.length - 1],
+        ),
+      );
+    }
+  }
+
+  private toResult(type: Type): InferenceResult {
+    return {
+      type,
+      expressionTypes: this.expressionTypes,
+      diagnostics: this.diagnostics,
+    };
+  }
+
+  private typeSignatureSegmentType(segment: Expression): Type {
+    let type: Type = TUnknown;
+
+    switch (segment.nodeType) {
+      case "TypeRef":
+        type = this.typeRefType(segment);
+        break;
+      case "TypeVariable":
+        type = this.typeVariableType(segment);
+        break;
+      case "TypeExpression":
+        type = this.typeExpressionType(segment);
+        break;
+      case "TupleType":
+        type = segment.unitExpr
+          ? TUnit
+          : TTuple(
+              segment.typeExpressions.map((t) => this.typeExpressionType(t)),
+            );
+        break;
+      case "RecordType":
+        type = this.recordTypeDeclarationType(segment);
+        break;
+    }
+
+    return type;
+  }
+
+  private typeVariableType(typeVariable: ETypeVariable): Type {
+    const definition = findDefinition(
+      typeVariable.firstNamedChild,
+      this.uri,
+      this.workspace.getImports(),
+    );
+
+    // The type variable doesn't reference anything
+    if (!definition || (<any>definition.expr).id === (<any>typeVariable).id) {
+      const type = this.getTypeVar(typeVariable);
+      this.expressionTypes.set(typeVariable, type);
+      return type;
+    }
+
+    const cached = this.varsByExpression.get(definition.expr);
+
+    if (cached) {
+      return cached;
+    }
+
+    const annotation = mapSyntaxNodeToExpression(
+      TreeUtils.getAllAncestorsOfType("type_annotation", definition.expr)[0],
+    );
+
+    const expr = annotation
+      ? TreeUtils.findFirstNamedChildOfType("type_expression", annotation)
+      : undefined;
+
+    // If the definition is not in a type annotation or it is to a
+    // variable in the same annotation, use the type of the reference
+    if (!annotation || !expr || (<any>annotation).id === (<any>this.root).id) {
+      const type = this.getTypeVar(definition.expr);
+      this.varsByExpression.set(typeVariable, type);
+      this.expressionTypes.set(typeVariable, type);
+      return type;
+    }
+
+    // If the definition is to a variable declared in a parent annotation,
+    // use the type from that annotation's inference
+    const type =
+      TypeExpression.typeAnnotationInference(
+        annotation as ETypeAnnotation,
+        definition.uri,
+        this.workspace,
+        true,
+      )?.expressionTypes.get(definition.expr) ?? TUnknown;
+
+    if (type.nodeType === "Var") {
+      this.varsByExpression.set(definition.expr, type);
+    }
+
+    this.expressionTypes.set(typeVariable, type);
+    return type;
+  }
+
+  private recordTypeDeclarationType(record: ERecordType): TRecord {
+    const fieldExpressions = record.fieldTypes;
+    if (!fieldExpressions || fieldExpressions.length === 0) {
+      return TRecord({});
+    }
+
+    const fieldTypes: { [key: string]: Type } = {};
+    fieldExpressions.forEach((field) => {
+      fieldTypes[field.name] = this.typeExpressionType(field.typeExpression);
+    });
+
+    const fieldRefs = RecordFieldReferenceTable.fromExpressions(
+      fieldExpressions,
+    );
+
+    const baseTypeDefinition = findDefinition(
+      record.baseType,
+      this.uri,
+      this.workspace.getImports(),
+    )?.expr;
+
+    const baseType = baseTypeDefinition
+      ? this.getTypeVar(baseTypeDefinition)
+      : record.baseType
+      ? TVar(record.baseType.text)
+      : undefined;
+
+    return TRecord(fieldTypes, baseType, undefined, fieldRefs);
+  }
+
+  private typeRefType(typeRef: ETypeRef): Type {
+    const args =
+      TreeUtils.findAllNamedChildrenOfType(
+        [
+          "type_variable",
+          "type_ref",
+          "tuple_type",
+          "record_type",
+          "type_expression",
+        ],
+        typeRef,
+      )
+        ?.map(mapSyntaxNodeToExpression)
+        .filter(Utils.notUndefined.bind(this))
+        .map((arg) => this.typeSignatureSegmentType(arg)) ?? [];
+
+    const definition = findDefinition(
+      typeRef.firstNamedChild?.lastNamedChild,
+      this.uri,
+      this.workspace.getImports(),
+    );
+
+    let declaredType: Type = TUnknown;
+    if (definition) {
+      switch (definition.expr.nodeType) {
+        case "TypeDeclaration":
+          declaredType = TypeExpression.typeDeclarationInference(
+            definition.expr,
+            definition.uri,
+            this.workspace,
+          ).type;
+          break;
+        case "TypeAliasDeclaration":
+          declaredType = TypeExpression.typeAliasDeclarationInference(
+            definition.expr,
+            definition.uri,
+            this.workspace,
+            new Set(this.activeAliases.values()),
+          ).type;
+          break;
+        default:
+          throw new Error("Unexpected type reference");
+      }
+    } else {
+      if (typeRef.firstNamedChild?.firstNamedChild?.text === "List") {
+        declaredType = TList(TVar("a"));
+      }
+    }
+
+    const params = declaredType?.alias
+      ? declaredType.alias.parameters
+      : declaredType?.nodeType === "Union"
+      ? declaredType.params
+      : [];
+
+    if (declaredType.nodeType !== "Unknown" && params.length !== args.length) {
+      this.diagnostics.push(
+        typeArgumentCountError(typeRef, args.length, params.length),
+      );
+      return TUnknown;
+    }
+
+    if (params.length === 0) {
+      return declaredType;
+    }
+
+    // The param types are always TVars
+    return TypeReplacement.replace(
+      declaredType,
+      new Map(
+        params
+          .map<[TVar, Type]>((p, i) => [<TVar>p, args[i]])
+          .filter(([, type]) => !!type),
+      ),
+    );
+  }
+
+  private typeDeclarationType(typeDeclaration: ETypeDeclaration): Type {
+    const params: TVar[] = typeDeclaration.typeNames.map((name) =>
+      this.getTypeVar(name),
+    );
+
+    return TUnion(typeDeclaration.moduleName, typeDeclaration.name, params);
+  }
+
+  private getTypeVar(e: Expression): TVar {
+    if (this.varsByExpression.has(e)) {
+      const tVar = this.varsByExpression.get(e);
+      if (tVar) {
+        return tVar;
+      }
+    }
+
+    const tVar = TVar(e.text, this.rigidVars);
+    this.varsByExpression.set(e, tVar);
+    return tVar;
+  }
+}
diff --git a/src/util/types/typeInference.ts b/src/util/types/typeInference.ts
new file mode 100644
index 0000000..caac668
--- /dev/null
+++ b/src/util/types/typeInference.ts
@@ -0,0 +1,2434 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { SyntaxNode } from "web-tree-sitter";
+import { flatMap, TreeUtils } from "../treeUtils";
+import { References } from "../references";
+import {
+  BinaryExprTree,
+  IOperatorPrecedence,
+  Operand,
+  Binary,
+} from "./operatorPrecedence";
+import { DisjointSet } from "./disjointSet";
+import { TypeReplacement } from "./typeReplacement";
+import {
+  Expression,
+  EValueDeclaration,
+  EFunctionCallExpr,
+  EBinOpExpr,
+  EOperator,
+  EFunctionDeclarationLeft,
+  EPattern,
+  mapSyntaxNodeToExpression,
+  findDefinition,
+  EIfElseExpr,
+  ELetInExpr,
+  ECaseOfExpr,
+  EAnonymousFunctionExpr,
+  ETuplePattern,
+  EListExpr,
+  EUnionPattern,
+  EListPattern,
+  EConsPattern,
+  ERecordPattern,
+  EFieldAccessorFunctionExpr,
+  EOperatorAsFunctionExpr,
+  ERecordExpr,
+  EFieldAccessExpr,
+} from "./expressionTree";
+import { SyntaxNodeMap } from "./syntaxNodeMap";
+import { TypeExpression } from "./typeExpression";
+import { IElmWorkspace } from "src/elmWorkspace";
+import { Sequence } from "../sequence";
+import { Utils } from "../utils";
+import { RecordFieldReferenceTable } from "./recordFieldReferenceTable";
+import { TypeRenderer } from "./typeRenderer";
+
+export interface Alias {
+  module: string;
+  name: string;
+  parameters: Type[];
+}
+
+export type Type =
+  | TVar
+  | TFunction
+  | TTuple
+  | TUnion
+  | TUnit
+  | TInProgressBinding
+  | TUnknown
+  | TRecord
+  | TMutableRecord;
+
+export interface TVar {
+  nodeType: "Var";
+  name: string;
+  rigid?: boolean;
+  alias?: Alias;
+}
+export interface TFunction {
+  nodeType: "Function";
+  params: Type[];
+  return: Type;
+  alias?: Alias;
+}
+export interface TTuple {
+  nodeType: "Tuple";
+  types: Type[];
+  alias?: Alias;
+}
+export interface TUnion {
+  nodeType: "Union";
+  module: string;
+  name: string;
+  params: Type[];
+  alias?: Alias;
+}
+export interface TRecord {
+  nodeType: "Record";
+  fields: { [key: string]: Type };
+  baseType?: Type;
+  alias?: Alias;
+  fieldReferences: RecordFieldReferenceTable;
+}
+export interface TMutableRecord {
+  nodeType: "MutableRecord";
+  fields: { [key: string]: Type };
+  baseType?: Type;
+  alias?: Alias;
+  fieldReferences: RecordFieldReferenceTable;
+}
+interface TUnit {
+  nodeType: "Unit";
+  alias?: Alias;
+}
+interface TInProgressBinding {
+  nodeType: "InProgressBinding";
+  alias?: Alias;
+}
+interface TUnknown {
+  nodeType: "Unknown";
+  alias?: Alias;
+}
+
+export const TUnion = (
+  module: string,
+  name: string,
+  params: Type[],
+  alias?: Alias,
+): TUnion => {
+  return { nodeType: "Union", module, name, params, alias };
+};
+
+export const TVar = (name: string, rigid = false): TVar => {
+  return { nodeType: "Var", name, rigid };
+};
+
+export const TFunction = (
+  params: Type[],
+  ret: Type,
+  alias?: Alias,
+): TFunction => {
+  return { nodeType: "Function", params, return: ret, alias };
+};
+
+export const TTuple = (types: Type[], alias?: Alias): TTuple => {
+  if (types.length === 0) {
+    throw new Error("Cannot create a TTuple with no types, use TUnit");
+  }
+
+  return { nodeType: "Tuple", types, alias };
+};
+
+export const TRecord = (
+  fields: { [key: string]: Type },
+  baseType?: Type,
+  alias?: Alias,
+  fieldReferences = new RecordFieldReferenceTable(),
+): TRecord => {
+  return {
+    nodeType: "Record",
+    fields,
+    baseType,
+    alias,
+    fieldReferences,
+  };
+};
+
+export const TMutableRecord = (
+  fields: { [key: string]: Type },
+  baseType?: Type,
+  fieldReferences = new RecordFieldReferenceTable(),
+): TMutableRecord => {
+  return {
+    nodeType: "MutableRecord",
+    fields,
+    baseType,
+    fieldReferences,
+  };
+};
+
+const TFloat = TUnion("Basics", "Float", []);
+const TBool = TUnion("Basics", "Bool", []);
+const TString = TUnion("String", "String", []);
+const TChar = TUnion("Char", "Char", []);
+
+export const TList = (elementType: Type): TUnion =>
+  TUnion("List", "List", [elementType]);
+
+const TNumber = TVar("number");
+
+export const TUnit: TUnit = {
+  nodeType: "Unit",
+};
+
+export const TInProgressBinding: TInProgressBinding = {
+  nodeType: "InProgressBinding",
+};
+
+export const TUnknown: TUnknown = {
+  nodeType: "Unknown",
+};
+
+function allTypeVars(type: Type): TVar[] {
+  switch (type.nodeType) {
+    case "Var":
+      return [type];
+    case "Union":
+      return type.params.flatMap(allTypeVars);
+    case "Function":
+      return allTypeVars(type.return).concat(type.params.flatMap(allTypeVars));
+    case "Tuple":
+      return type.types.flatMap(allTypeVars);
+    case "Record":
+    case "MutableRecord": {
+      return [
+        ...Object.values(type.fields).flatMap(allTypeVars),
+        ...(type.baseType ? allTypeVars(type.baseType) : []),
+      ];
+    }
+    case "Unknown":
+    case "Unit":
+    case "InProgressBinding":
+      return [];
+  }
+}
+
+function anyTypeVar(type: Type, predicate: (tvar: TVar) => boolean): boolean {
+  let result = false;
+  switch (type.nodeType) {
+    case "Var":
+      result = predicate(type);
+      break;
+    case "Union":
+      result = type.params.some((param) => anyTypeVar(param, predicate));
+      break;
+    case "Function":
+      result =
+        anyTypeVar(type.return, predicate) ||
+        type.params.some((param) => anyTypeVar(param, predicate));
+      break;
+    case "Record":
+    case "MutableRecord":
+      result =
+        Object.values(type.fields).some((field) =>
+          anyTypeVar(field, predicate),
+        ) || (type.baseType ? anyTypeVar(type.baseType, predicate) : false);
+      break;
+    case "Unknown":
+      result = false;
+      break;
+  }
+
+  return (
+    result ||
+    type.alias?.parameters.some((param) => anyTypeVar(param, predicate)) ===
+      true
+  );
+}
+
+/**
+ * Curry function helper
+ * @param func The function type to curry
+ * @param count The number params to partially apply
+ */
+export function curryFunction(func: TFunction, count: number): Type {
+  if (count < func.params.length) {
+    return TFunction(func.params.slice(count), func.return);
+  } else {
+    return func.return;
+  }
+}
+
+/**
+ * Uncurry function helper
+ * @param func The function type to uncurry
+ */
+export function uncurryFunction(func: TFunction): TFunction {
+  if (func.return.nodeType === "Function") {
+    return TFunction(
+      [...func.params, ...func.return.params],
+      func.return.return,
+    );
+  } else {
+    return func;
+  }
+}
+
+function getParentPatternDeclaration(
+  ref: Expression,
+): EValueDeclaration | undefined {
+  const parentPattern = mapSyntaxNodeToExpression(
+    TreeUtils.findParentOfType("value_declaration", ref),
+  );
+
+  if (parentPattern?.nodeType !== "ValueDeclaration") {
+    throw new Error(`Failed to get parent pattern declaration`);
+  }
+
+  return parentPattern.pattern ? parentPattern : undefined;
+}
+
+export function getTypeclassName(type: TVar): string | undefined {
+  switch (type.name) {
+    case "number":
+    case "appendable":
+    case "comparable":
+    case "compappend":
+      return type.name;
+  }
+}
+
+const VAR_LETTERS = "abcdefghijklmnopqrstuvwxyz";
+
+export function getVarNames(count: number): string[] {
+  const names = [];
+  for (let i = 0; i < count; i++) {
+    const letter = VAR_LETTERS[i % 26];
+    if (i < 26) {
+      names.push(letter);
+    } else {
+      names.push(`${letter}${i / 26}`);
+    }
+  }
+  return names;
+}
+
+export function nthVarName(n: number): string {
+  return getVarNames(n)[n - 1];
+}
+
+function typeMismatchError(
+  node: SyntaxNode,
+  found: Type,
+  expected: Type,
+  endNode?: SyntaxNode,
+  patternBinding = false,
+  recordDiff?: RecordDiff,
+): Diagnostic {
+  const foundText = TypeRenderer.typeToString(found);
+  const expectedText = TypeRenderer.typeToString(expected);
+
+  const message = patternBinding
+    ? `Invalid pattern error\nExpected: ${expectedText}\nFound: ${foundText}`
+    : `Type mismatch error\nExpected: ${expectedText}\nFound: ${foundText}`;
+
+  const recordDiffText = (diff: RecordDiff): string => {
+    let s = "";
+
+    if (diff.extra.size > 0) {
+      s += `\nExtra fields: ${TypeRenderer.typeToString(
+        TRecord(Object.fromEntries(diff.extra.entries())),
+      )}`;
+    }
+    if (diff.missing.size > 0) {
+      s += `\nMissing fields: ${TypeRenderer.typeToString(
+        TRecord(Object.fromEntries(diff.missing.entries())),
+      )}`;
+    }
+    if (diff.mismatched.size > 0) {
+      s += `\nMismatched fields: `;
+      diff.mismatched.forEach(([expected, found], field) => {
+        s += `\n Field ${field} expected ${TypeRenderer.typeToString(
+          expected,
+        )}, found ${TypeRenderer.typeToString(found)}`;
+      });
+    }
+
+    return s;
+  };
+
+  return {
+    node,
+    endNode: endNode ?? node,
+    message: message + (recordDiff ? recordDiffText(recordDiff) : ""),
+  };
+}
+
+function parameterCountError(
+  node: SyntaxNode,
+  endNode: SyntaxNode,
+  actual: number,
+  expected: number,
+  isType = false,
+): Diagnostic {
+  const name = node.firstNamedChild?.text ?? "";
+  return {
+    node,
+    endNode,
+    message: `The ${
+      isType ? "type" : "function"
+    } \`${name}\` expects ${expected} argument${
+      expected !== 1 ? `s` : ``
+    }, but got ${actual} instead`,
+  };
+}
+
+function argumentCountError(
+  node: SyntaxNode,
+  endNode: SyntaxNode,
+  actual: number,
+  expected: number,
+  isType = false,
+): Diagnostic {
+  if (expected === 0 && !isType) {
+    const name = node.firstNamedChild?.text;
+    return {
+      node,
+      endNode,
+      message: `${
+        name ? `\`${name}\`` : "This value"
+      } is not a function, but it was given ${actual} argument${
+        actual !== 1 ? `s` : ``
+      }`,
+    };
+  } else {
+    return parameterCountError(node, endNode, actual, expected, isType);
+  }
+}
+
+function redefinitionError(node: SyntaxNode): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `A value named \`${node.text}\` is already defined`,
+  };
+}
+
+export function badRecursionError(node: SyntaxNode): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `Infinite recursion`,
+  };
+}
+
+function cyclicDefinitionError(node: SyntaxNode): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `Value cannot be defined in terms of itself`,
+  };
+}
+
+function partialPatternError(node: SyntaxNode): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `Pattern does not cover all posiblities`,
+  };
+}
+
+export function typeArgumentCountError(
+  node: SyntaxNode,
+  actual: number,
+  expected: number,
+): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `The type expected ${expected} argument${
+      expected !== 1 ? "s" : ""
+    }, but got ${actual} instead`,
+  };
+}
+
+function recordFieldError(node: SyntaxNode, field: string): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `The record does not have a \`${field}\` field`,
+  };
+}
+
+function recordBaseIdError(node: SyntaxNode, type: Type): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `Type must be a record, instead found: ${TypeRenderer.typeToString(
+      type,
+    )}`,
+  };
+}
+
+function fieldAccessOnNonRecordError(node: SyntaxNode, type: Type): Diagnostic {
+  return {
+    node,
+    endNode: node,
+    message: `Cannot access fields on non-record type: ${TypeRenderer.typeToString(
+      type,
+    )}`,
+  };
+}
+
+interface RecordDiff {
+  extra: Map<string, Type>;
+  missing: Map<string, Type>;
+  mismatched: Map<string, [Type, Type]>;
+  isEmpty: boolean;
+}
+
+const RecordDiff = (
+  extra: Map<string, Type>,
+  missing: Map<string, Type>,
+  mismatched: Map<string, [Type, Type]>,
+): RecordDiff => {
+  return {
+    extra,
+    missing,
+    mismatched,
+    isEmpty: extra.size === 0 && missing.size === 0 && mismatched.size === 0,
+  };
+};
+
+export interface Diagnostic {
+  node: SyntaxNode;
+  endNode: SyntaxNode;
+  message: string;
+}
+
+export interface InferenceResult {
+  expressionTypes: SyntaxNodeMap<Expression, Type>;
+  diagnostics: Diagnostic[];
+  type: Type;
+}
+
+export class InferenceScope {
+  private expressionTypes: SyntaxNodeMap<
+    Expression,
+    Type
+  > = new SyntaxNodeMap();
+  private diagnostics: Diagnostic[] = [];
+
+  private bindings: Map<string, Type> = new Map<string, Type>();
+  private replacements: DisjointSet;
+
+  private annotationVars: TVar[] = [];
+
+  public ancestors: Sequence<InferenceScope>;
+
+  private resolvedDeclarations: SyntaxNodeMap<EValueDeclaration, Type>;
+
+  private childDeclarations = new Set<EValueDeclaration>();
+
+  private recordDiffs: SyntaxNodeMap<
+    Expression,
+    RecordDiff
+  > = new SyntaxNodeMap();
+
+  constructor(
+    private uri: string,
+    private elmWorkspace: IElmWorkspace,
+    private nonShadowableNames: Set<string>,
+    private activeScopes: Set<EValueDeclaration>,
+    private recursionAllowed: boolean,
+    private parent?: InferenceScope,
+  ) {
+    this.replacements = parent?.replacements ?? new DisjointSet();
+
+    this.resolvedDeclarations =
+      parent?.resolvedDeclarations ??
+      new SyntaxNodeMap<EValueDeclaration, Type>();
+
+    this.ancestors = new Sequence(
+      this,
+      (scope: InferenceScope) => scope.parent,
+    );
+  }
+
+  private getBinding(e: Expression): Type | undefined {
+    return this.ancestors
+      .map((a) => a.bindings.get(e.text))
+      .find(Utils.notUndefined.bind(this));
+  }
+
+  public static valueDeclarationInference(
+    declaration: EValueDeclaration,
+    uri: string,
+    elmWorkspace: IElmWorkspace,
+    activeScopes: Set<EValueDeclaration>,
+  ): InferenceResult {
+    // TODO: Need a good way to get all visible values
+    const shadowableNames = new Set<string>();
+
+    const setter = (): InferenceResult =>
+      new InferenceScope(
+        uri,
+        elmWorkspace,
+        shadowableNames,
+        activeScopes,
+        false,
+      ).inferDeclaration(declaration, true);
+
+    if (!elmWorkspace.getForest().getByUri(uri)?.writeable) {
+      return elmWorkspace
+        .getTypeCache()
+        .getOrSet("PACKAGE_VALUE", declaration, setter);
+    } else {
+      return setter();
+    }
+  }
+
+  private inferDeclaration(
+    declaration: EValueDeclaration,
+    replaceExpressionTypes: boolean,
+  ): InferenceResult {
+    this.activeScopes.add(declaration);
+
+    // Bind the parameters so the body can reference them
+    const binding = this.bindParameters(declaration);
+
+    // If there is a pattern, it gets inferred in the parameter binding
+    if (declaration.pattern) {
+      return this.toTopLevelResult(TUnknown);
+    }
+
+    let bodyType: Type = TUnknown;
+    if (declaration.body) {
+      bodyType = this.infer(declaration.body);
+
+      // Make sure the returned type is what is annotated
+      if (binding.bindingType === "Annotated") {
+        const bindingType = (<Annotated>binding).type;
+        const expected =
+          bindingType.nodeType === "Function"
+            ? curryFunction(bindingType, binding.count)
+            : bindingType;
+        this.isAssignable(declaration.body, bodyType, expected);
+      } else {
+        this.checkTopLevelCaseBranches(declaration.body, bodyType);
+      }
+    }
+
+    const type: Type =
+      binding.bindingType === "Annotated"
+        ? (<Annotated>binding).type
+        : binding.bindingType === "Unannotated"
+        ? binding.count === 0
+          ? bodyType
+          : uncurryFunction(TFunction((<Unannotated>binding).params, bodyType))
+        : bodyType;
+
+    return this.toTopLevelResult(type, replaceExpressionTypes);
+  }
+
+  private checkTopLevelCaseBranches(expr: Expression, exprType: Type): void {
+    if (expr.nodeType === "CaseOfExpr") {
+      this.isBranchesAssignable(expr, exprType, TUnknown);
+    }
+  }
+
+  private checkRecursion(declaration: EValueDeclaration): boolean {
+    const isRecursive = this.activeScopes.has(declaration);
+
+    const functionDeclaration = mapSyntaxNodeToExpression(
+      TreeUtils.findFirstNamedChildOfType(
+        "function_declaration_left",
+        declaration,
+      ),
+    );
+    if (
+      isRecursive &&
+      !this.recursionAllowed &&
+      (functionDeclaration?.nodeType !== "FunctionDeclarationLeft" ||
+        !functionDeclaration.params[0])
+    ) {
+      this.diagnostics.push(badRecursionError(declaration));
+    }
+
+    return isRecursive;
+  }
+
+  private toTopLevelResult(
+    type: Type,
+    replaceExpressionTypes = true,
+  ): InferenceResult {
+    if (replaceExpressionTypes) {
+      this.expressionTypes.mapValues((val) => {
+        const result = TypeReplacement.replace(val, this.replacements.toMap());
+        TypeReplacement.freeze(result);
+        return result;
+      });
+    }
+
+    const outerVars = flatMap(
+      this.ancestors.toArray().slice(1),
+      (a) => a.annotationVars,
+    );
+
+    const ret = TypeReplacement.replace(
+      type,
+      this.replacements.toMap(),
+      false,
+      outerVars,
+    );
+
+    if (replaceExpressionTypes) {
+      TypeReplacement.freeze(ret);
+    }
+
+    return {
+      expressionTypes: this.expressionTypes,
+      diagnostics: this.diagnostics,
+      type: ret,
+    };
+  }
+
+  private infer(e: Expression): Type {
+    let type: Type = TUnknown;
+
+    switch (e.nodeType) {
+      case "AnonymousFunctionExpr":
+        type = this.inferLambda(e);
+        break;
+      case "BinOpExpr":
+        type = this.inferBinOpExpr(e);
+        break;
+      case "CaseOfExpr":
+        type = this.inferCase(e);
+        break;
+      case "CharConstant":
+        type = TChar;
+        break;
+      case "FieldAccessExpr":
+        type = this.inferFieldAccess(e);
+        break;
+      case "FieldAccessorFunctionExpr":
+        type = this.inferFieldAccessorFunctionExpr(e);
+        break;
+      case "FunctionCallExpr":
+        type = this.inferFunctionCallExpr(e);
+        break;
+      case "IfElseExpr":
+        type = this.inferIfElse(e);
+        break;
+      case "LetInExpr":
+        type = this.inferChild((inference) => inference.letInInference(e)).type;
+        break;
+      case "ListExpr":
+        type = this.inferList(e);
+        break;
+      case "NumberConstant":
+        type = e.isFloat ? TFloat : TNumber;
+        break;
+      case "OperatorAsFunctionExpr":
+        type = this.inferOperatorAsFunctionExpr(e);
+        break;
+      case "RecordExpr":
+        type = this.inferRecord(e);
+        break;
+      case "StringConstant":
+        type = TString;
+        break;
+      case "TupleExpr":
+        type = TTuple(e.exprList.map((expr) => this.infer(expr)));
+        break;
+      case "UnitExpr":
+        type = TUnit;
+        break;
+      case "ValueExpr":
+        type = this.inferReferenceElement(e);
+        break;
+      default:
+        throw new Error("Unexpected Expression type");
+    }
+
+    this.expressionTypes.set(e, type);
+    return type;
+  }
+
+  private lambdaInference(lamba: EAnonymousFunctionExpr): InferenceResult {
+    const paramVars = this.uniqueVars(lamba.params.length);
+    lamba.params.forEach((p, i) => this.bindPattern(p, paramVars[i], true));
+
+    const bodyType = this.infer(lamba.expr);
+    this.checkTopLevelCaseBranches(lamba.expr, bodyType);
+
+    return {
+      expressionTypes: this.expressionTypes,
+      diagnostics: this.diagnostics,
+      type: uncurryFunction(TFunction(paramVars, bodyType)),
+    };
+  }
+
+  private letInInference(letInExpr: ELetInExpr): InferenceResult {
+    const valueDeclarations = letInExpr.valueDeclarations;
+    valueDeclarations.forEach((v) => this.childDeclarations.add(v));
+
+    valueDeclarations.forEach((declaration) => {
+      if (!this.resolvedDeclarations.has(declaration)) {
+        this.inferChildDeclaration(declaration);
+      }
+    });
+
+    const bodyType = this.infer(letInExpr.body);
+    this.checkTopLevelCaseBranches(letInExpr.body, bodyType);
+
+    return {
+      expressionTypes: this.expressionTypes,
+      diagnostics: this.diagnostics,
+      type: bodyType,
+    };
+  }
+
+  private caseBranchInference(
+    pattern: EPattern,
+    caseType: Type,
+    branchExpr: Expression,
+  ): InferenceResult {
+    this.bindPattern(pattern, caseType, false);
+    const type = this.infer(branchExpr);
+    return {
+      expressionTypes: this.expressionTypes,
+      diagnostics: this.diagnostics,
+      type,
+    };
+  }
+
+  private inferChild(
+    callback: (inference: InferenceScope) => InferenceResult,
+    activeScopes = new Set(this.activeScopes.values()),
+    recursionAllowed = this.recursionAllowed,
+  ): InferenceResult {
+    const result = callback(
+      new InferenceScope(
+        this.uri,
+        this.elmWorkspace,
+        this.nonShadowableNames,
+        activeScopes,
+        recursionAllowed,
+        this,
+      ),
+    );
+
+    this.diagnostics.push(...result.diagnostics);
+
+    result.expressionTypes.forEach((val, key) =>
+      this.expressionTypes.set(key, val),
+    );
+
+    return result;
+  }
+
+  private inferChildDeclaration(
+    declaration: EValueDeclaration,
+    activeScopes = new Set(this.activeScopes.values()),
+  ): InferenceResult {
+    const result = this.inferChild(
+      (inference) => inference.inferDeclaration(declaration, false),
+      activeScopes,
+    );
+
+    this.diagnostics.push(...result.diagnostics);
+
+    result.expressionTypes.forEach((val, key) =>
+      this.expressionTypes.set(key, val),
+    );
+
+    const funcName = TreeUtils.getFunctionNameNodeFromDefinition(declaration)
+      ?.text;
+
+    if (funcName) {
+      this.nonShadowableNames.add(funcName);
+    } else {
+      const pattern = declaration.pattern;
+
+      if (pattern) {
+        const patterns = pattern
+          .descendantsOfType("lower_pattern")
+          ?.map(mapSyntaxNodeToExpression)
+          .filter(Utils.notUndefined.bind(this));
+
+        patterns?.forEach((pat) => {
+          const patType = result.expressionTypes.get(pat);
+          if (patType) {
+            this.setBinding(pat, patType);
+          }
+          this.nonShadowableNames.add(pat.text);
+        });
+      }
+    }
+
+    return result;
+  }
+
+  private inferList(expr: EListExpr): Type {
+    const exprTypes = expr.exprList.map((e) => this.infer(e));
+
+    for (let i = 1; i < expr.exprList.length; i++) {
+      if (this.isAssignable(expr.exprList[i], exprTypes[i], exprTypes[0])) {
+        this.expressionTypes.set(expr.exprList[i], exprTypes[0]);
+      } else {
+        break;
+      }
+    }
+
+    return TList(exprTypes[0] ?? TVar("a"));
+  }
+
+  private inferIfElse(ifElseExpr: EIfElseExpr): Type {
+    const exprList = ifElseExpr.exprList;
+
+    // Check for incomplete program
+    if (exprList.length < 3 || exprList.length % 2 == 0) {
+      return TUnknown;
+    }
+
+    const exprTypes = exprList.map((e) => this.infer(e));
+
+    // Check all conditions are type Bool
+    for (let i = 0; i < exprList.length - 1; i += 2) {
+      this.isAssignable(exprList[i], exprTypes[i], TBool);
+    }
+
+    // Check that all branches match the first one
+    for (let i = 0; i < exprList.length; i++) {
+      if (i != exprList.length - 1 && (i < 3 || i % 2 == 0)) {
+        continue;
+      }
+
+      if (!this.isAssignable(exprList[i], exprTypes[i], exprTypes[1])) {
+        break;
+      }
+    }
+
+    return exprTypes[1];
+  }
+
+  private inferReferenceElement(e: Expression): Type {
+    const definition =
+      findDefinition(
+        e.firstNamedChild?.lastNamedChild,
+        this.uri,
+        this.elmWorkspace.getImports(),
+      ) ??
+      findDefinition(
+        e.firstNamedChild,
+        this.uri,
+        this.elmWorkspace.getImports(),
+      );
+
+    if (!definition) {
+      return TUnknown;
+    }
+
+    const binding = this.getBinding(definition.expr);
+
+    if (binding) {
+      if (binding.nodeType === "InProgressBinding") {
+        this.diagnostics.push(cyclicDefinitionError(e));
+        return TUnknown;
+      } else {
+        return binding;
+      }
+    }
+
+    switch (definition.expr.nodeType) {
+      case "FunctionDeclarationLeft": {
+        const valueDeclaration = mapSyntaxNodeToExpression(
+          TreeUtils.findParentOfType("value_declaration", definition.expr),
+        );
+
+        if (valueDeclaration?.nodeType !== "ValueDeclaration") {
+          throw new Error("Could not find parent value declaration");
+        }
+        return this.inferReferencedValueDeclaration(
+          valueDeclaration,
+          definition?.uri,
+        );
+      }
+      case "LowerPattern": {
+        const parentPattern = getParentPatternDeclaration(definition.expr);
+        if (parentPattern) {
+          this.inferReferencedValueDeclaration(parentPattern, definition.uri);
+
+          const binding = this.getBinding(definition.expr);
+
+          if (binding) {
+            return binding;
+          } else {
+            throw new Error("Failed to destructure pattern");
+          }
+        }
+
+        throw new Error("Failed to bind pattern");
+      }
+      case "UnionVariant": {
+        return TypeExpression.unionVariantInference(
+          definition.expr,
+          this.uri,
+          this.elmWorkspace,
+        ).type;
+      }
+      case "TypeAliasDeclaration": {
+        const ty = TypeExpression.typeAliasDeclarationInference(
+          definition.expr,
+          this.uri,
+          this.elmWorkspace,
+        ).type;
+        if (ty.nodeType === "Record" && Object.keys(ty.fields).length > 0) {
+          return TFunction(Object.values(ty.fields), ty);
+        } else {
+          return ty;
+        }
+      }
+      case "FieldType": {
+        const typeAlias = mapSyntaxNodeToExpression(
+          TreeUtils.findParentOfType("type_alias_declaration", definition.expr),
+        );
+
+        if (typeAlias && typeAlias.nodeType === "TypeAliasDeclaration") {
+          const fields = (TypeExpression.typeAliasDeclarationInference(
+            typeAlias,
+            this.uri,
+            this.elmWorkspace,
+          ).type as TRecord)?.fields;
+
+          if (fields) {
+            return fields[definition.expr.text];
+          }
+        }
+
+        return TUnknown;
+      }
+      case "PortAnnotation": {
+        return TypeExpression.portAnnotationInference(
+          definition.expr,
+          this.uri,
+          this.elmWorkspace,
+        ).type;
+      }
+      default:
+        throw new Error("Unexpected reference type");
+    }
+  }
+
+  private inferReferencedValueDeclaration(
+    declaration: EValueDeclaration | undefined,
+    referenceUri: string,
+  ): Type {
+    if (!declaration) {
+      return TUnknown;
+    }
+
+    const recursive = this.checkRecursion(declaration);
+
+    const existing = this.resolvedDeclarations.get(declaration);
+
+    if (existing) {
+      return TypeReplacement.freshenVars(existing);
+    }
+
+    let type: Type | undefined;
+    // Get the type annotation if there is one
+    if (declaration.typeAnnotation) {
+      type = TypeExpression.typeAnnotationInference(
+        declaration.typeAnnotation,
+        referenceUri,
+        this.elmWorkspace,
+        false,
+      )?.type;
+    }
+
+    if (!type) {
+      // Don't try to infer unannotated recursive functions
+      if (recursive) {
+        return TUnknown;
+      }
+
+      const parentScope = this.ancestors.find(
+        (scope) =>
+          !!Array.from(scope.childDeclarations.values()).find(
+            (child) => (<any>child).id === (<any>declaration).id,
+          ),
+      );
+
+      type = !parentScope
+        ? InferenceScope.valueDeclarationInference(
+            declaration,
+            referenceUri,
+            this.elmWorkspace,
+            this.activeScopes,
+          ).type
+        : parentScope.inferChildDeclaration(declaration, this.activeScopes)
+            .type;
+    }
+
+    this.resolvedDeclarations.set(declaration, type);
+    return type;
+  }
+
+  private inferFunctionCallExpr(e: EFunctionCallExpr): Type {
+    const targetType = this.infer(e.target);
+    const argTypes = e.args.map((arg) => this.infer(arg));
+
+    const argCountError = (
+      expr: Expression,
+      endExpr: Expression,
+      actual: number,
+      expected: number,
+    ): TUnknown => {
+      this.diagnostics.push(
+        argumentCountError(expr, endExpr, actual, expected),
+      );
+      return TUnknown;
+    };
+
+    if (targetType.nodeType === "Var") {
+      const type = TFunction(argTypes, TVar("a"));
+
+      if (this.isAssignable(e.target, targetType, type)) {
+        return type.return;
+      } else {
+        return TUnknown;
+      }
+    }
+
+    if (targetType.nodeType === "Unknown") {
+      return TUnknown;
+    }
+
+    if (targetType.nodeType !== "Function") {
+      return argCountError(e, e, e.args.length, 0);
+    }
+
+    let allAssignable = true;
+
+    // Make sure each arg is assignable to the inferred type
+    for (
+      let i = 0;
+      i < Math.min(e.args.length, targetType.params.length);
+      i++
+    ) {
+      allAssignable =
+        this.isAssignable(e.args[i], argTypes[i], targetType.params[i]) &&
+        allAssignable;
+    }
+
+    if (allAssignable && e.args.length > targetType.params.length) {
+      let appliedType = TypeReplacement.replace(
+        targetType.return,
+        this.replacements.toMap(),
+      );
+
+      if (appliedType.nodeType !== "Function") {
+        return argCountError(e, e, e.args.length, targetType.params.length);
+      }
+
+      for (let i = targetType.params.length; i < e.args.length - 1; i++) {
+        if (appliedType.nodeType !== "Function") {
+          return argCountError(e.target, e.args[i], 1, 0);
+        }
+
+        if (!this.isAssignable(e.args[i], argTypes[i], appliedType.params[0])) {
+          return TUnknown;
+        }
+
+        appliedType = TypeReplacement.replace(
+          curryFunction(appliedType, 1),
+          this.replacements.toMap(),
+        );
+      }
+
+      this.expressionTypes.set(e, appliedType);
+      return appliedType;
+    }
+
+    const resultType: Type = allAssignable
+      ? TypeReplacement.replace(
+          curryFunction(targetType, e.args.length),
+          this.replacements.toMap(),
+          true,
+        )
+      : TUnknown;
+
+    this.expressionTypes.set(e, resultType);
+    return resultType;
+  }
+
+  private inferBinOpExpr(e: EBinOpExpr): Type {
+    const operatorPrecedences = new SyntaxNodeMap<
+      Expression,
+      IOperatorPrecedence
+    >();
+    const operatorTypes = new SyntaxNodeMap<Expression, TFunction>();
+
+    let lastPrecedence: IOperatorPrecedence | undefined;
+
+    for (const part of e.parts) {
+      if (part.nodeType === "Operator") {
+        const [type, precedence] = this.inferOperatorAndPrecedence(part);
+        if (type.nodeType !== "Function" || type.params.length < 2) {
+          return TUnknown;
+        }
+
+        if (
+          precedence.associativity === "NON" &&
+          lastPrecedence?.associativity === "NON"
+        ) {
+          this.diagnostics.push({
+            node: e,
+            endNode: e,
+            message: "NonAssociativeOperatorError",
+          });
+          return TUnknown;
+        }
+
+        operatorPrecedences.set(part, precedence);
+        operatorTypes.set(part, type);
+
+        lastPrecedence = precedence;
+      }
+    }
+
+    const validateTree = (
+      tree: BinaryExprTree,
+    ): { start: Expression; end: Expression; type: Type } => {
+      switch (tree.type) {
+        case "Operand": {
+          const operandTree = <Operand>tree;
+          const type = this.inferOperand(operandTree.operand);
+          return { start: operandTree.operand, end: operandTree.operand, type };
+        }
+        case "Binary": {
+          const binaryTree = <Binary>tree;
+          const left = validateTree(binaryTree.left);
+          const right = validateTree(binaryTree.right);
+          const func = operatorTypes.get(binaryTree.operator);
+
+          if (!func) {
+            throw new Error("Missing function type for operator");
+          }
+
+          const leftAssignable = this.isAssignable(
+            left.start,
+            left.type,
+            func.params[0],
+            left.end,
+          );
+          const rightAssignable = this.isAssignable(
+            right.start,
+            right.type,
+            func.params[1],
+            right.end,
+          );
+
+          const type: Type =
+            leftAssignable && rightAssignable
+              ? TypeReplacement.replace(
+                  curryFunction(func, 2),
+                  this.replacements.toMap(),
+                  true,
+                )
+              : TUnknown;
+
+          return { start: left.start, end: right.end, type };
+        }
+      }
+    };
+
+    const result = validateTree(
+      BinaryExprTree.parse(e.parts, operatorPrecedences),
+    );
+
+    this.expressionTypes.set(e, result.type);
+
+    return result.type;
+  }
+
+  private inferOperand(e: Expression): Type {
+    if (e.nodeType === "FunctionCallExpr") {
+      return this.inferFunctionCallExpr(e);
+    }
+
+    return this.infer(e);
+  }
+
+  private inferOperatorAndPrecedence(
+    e: EOperator,
+  ): [Type, IOperatorPrecedence] {
+    // Find operator reference
+    const definition = findDefinition(
+      e,
+      this.uri,
+      this.elmWorkspace.getImports(),
+    );
+
+    const opDeclaration = mapSyntaxNodeToExpression(definition?.expr.parent);
+
+    const infixDeclarationExpr = mapSyntaxNodeToExpression(
+      opDeclaration ? References.findOperator(opDeclaration) : undefined,
+    );
+
+    if (
+      opDeclaration?.nodeType !== "ValueDeclaration" ||
+      infixDeclarationExpr?.nodeType !== "InfixDeclaration" ||
+      !definition
+    ) {
+      return [TUnknown, { precedence: 0, associativity: "NON" }];
+    }
+
+    const type = this.inferReferencedValueDeclaration(
+      opDeclaration,
+      definition.uri,
+    );
+
+    this.expressionTypes.set(e, type);
+    return [
+      type,
+      {
+        precedence: infixDeclarationExpr.precedence,
+        associativity: infixDeclarationExpr.associativity,
+      },
+    ];
+  }
+
+  private inferFieldAccessorFunctionExpr(
+    accessor: EFieldAccessorFunctionExpr,
+  ): Type {
+    const field =
+      TreeUtils.findFirstNamedChildOfType("lower_case_identifier", accessor)
+        ?.text ?? "";
+    const typeVar = TVar("b");
+    return TFunction(
+      [TMutableRecord({ [field]: typeVar }, TVar("a"))],
+      typeVar,
+    );
+  }
+
+  private inferOperatorAsFunctionExpr(operator: EOperatorAsFunctionExpr): Type {
+    // Find operator reference
+    const definition = findDefinition(
+      operator,
+      this.uri,
+      this.elmWorkspace.getImports(),
+    );
+
+    const opDeclaration = mapSyntaxNodeToExpression(definition?.expr.parent);
+
+    if (opDeclaration?.nodeType === "ValueDeclaration" && definition?.uri) {
+      return this.inferReferencedValueDeclaration(
+        opDeclaration,
+        definition.uri,
+      );
+    } else {
+      return TUnknown;
+    }
+  }
+
+  private inferFieldAccess(expr: EFieldAccessExpr): Type {
+    const targetType = this.inferFieldAccessTarget(expr.target);
+    const targetTy = this.replacements.get(targetType);
+    const fieldIdentifier = TreeUtils.findFirstNamedChildOfType(
+      "lower_case_identifier",
+      expr,
+    );
+
+    if (!fieldIdentifier) {
+      return TUnknown;
+    }
+
+    if (targetTy?.nodeType === "Var") {
+      if (targetTy.rigid) {
+        this.diagnostics.push(recordBaseIdError(expr.target, targetTy));
+        return TUnknown;
+      }
+
+      const type = TVar("b");
+      this.trackReplacement(
+        targetType,
+        TMutableRecord({ [fieldIdentifier.text]: type }, TVar("a")),
+      );
+      this.expressionTypes.set(expr, type);
+      return type;
+    }
+
+    if (targetTy?.nodeType === "MutableRecord") {
+      let type = targetTy.fields[fieldIdentifier.text];
+      if (!type) {
+        targetTy.fields[fieldIdentifier.text] = TVar(
+          nthVarName(Object.keys(targetTy.fields).length),
+        );
+        type = targetTy.fields[fieldIdentifier.text];
+      }
+
+      this.expressionTypes.set(expr, type);
+
+      return type;
+    }
+
+    if (targetTy?.nodeType !== "Record") {
+      if (targetTy?.nodeType !== "Unknown" && targetTy) {
+        this.diagnostics.push(
+          fieldAccessOnNonRecordError(expr.target, targetTy),
+        );
+      }
+      return TUnknown;
+    }
+
+    if (!Object.keys(targetTy.fields).includes(fieldIdentifier.text)) {
+      if (!targetTy.baseType) {
+        this.diagnostics.push(
+          recordFieldError(fieldIdentifier, fieldIdentifier.text),
+        );
+      }
+    }
+
+    const type = targetTy.fields[fieldIdentifier.text];
+    this.expressionTypes.set(expr, type);
+    return type;
+  }
+
+  private inferFieldAccessTarget(target: Expression): Type {
+    let type: Type = TUnknown;
+    switch (target.nodeType) {
+      case "ValueExpr":
+        type = this.inferReferenceElement(target);
+        break;
+      case "RecordExpr":
+        type = this.inferRecord(target);
+        break;
+      case "FieldAccessExpr":
+        type = this.inferFieldAccess(target);
+        break;
+      default:
+        throw new Error("Unexpected field access target expression");
+    }
+
+    this.expressionTypes.set(target, type);
+    return type;
+  }
+
+  private inferLambda(lambaExpr: EAnonymousFunctionExpr): Type {
+    return this.inferChild(
+      (inference) => inference.lambdaInference(lambaExpr),
+      undefined,
+      true,
+    ).type;
+  }
+
+  private inferCase(caseOfExpr: ECaseOfExpr): Type {
+    const caseOfExprType = this.infer(caseOfExpr.expr);
+
+    let type: Type | undefined;
+
+    caseOfExpr.branches.forEach((branch) => {
+      const result = this.inferChild((inference) =>
+        inference.caseBranchInference(
+          branch.pattern,
+          caseOfExprType,
+          branch.expr,
+        ),
+      );
+
+      if (!type) {
+        type = result.type;
+      }
+    });
+
+    return type ?? TUnknown;
+  }
+
+  private inferRecord(record: ERecordExpr): Type {
+    const fields = new Map(
+      record.fields.map((field) => [field.name, this.infer(field.expression)]),
+    );
+
+    const mappedFields: { [name: string]: Type } = {};
+    fields.forEach((type, field) => (mappedFields[field.text] = type));
+
+    const recordIdentifier = record.baseRecord as Expression;
+
+    if (!recordIdentifier) {
+      return TRecord(mappedFields);
+    }
+
+    const baseType = this.inferReferenceElement(recordIdentifier);
+
+    if (baseType.nodeType === "Unknown") {
+      return TUnknown;
+    }
+
+    if (baseType.nodeType === "Var") {
+      const extRecord = TRecord(mappedFields, TVar(baseType.name));
+      if (this.isAssignable(recordIdentifier, baseType, extRecord)) {
+        return extRecord;
+      } else {
+        return TUnknown;
+      }
+    }
+
+    let baseFields: { [name: string]: Type } = {};
+
+    if (
+      baseType.nodeType === "Record" ||
+      baseType.nodeType === "MutableRecord"
+    ) {
+      baseFields = baseType.fields;
+    } else {
+      this.diagnostics.push({
+        node: recordIdentifier,
+        endNode: recordIdentifier,
+        message: "RecordBaseIdError",
+      });
+      return TUnknown;
+    }
+
+    fields.forEach((type, field) => {
+      const expected = baseFields[field.text];
+      if (!expected) {
+        if (baseType.nodeType === "Record") {
+          if (!baseType.baseType) {
+            this.diagnostics.push(recordFieldError(field, field.text));
+            this.recordDiffs.set(
+              record,
+              this.calculateRecordDiff(TRecord(mappedFields), baseType),
+            );
+          }
+        } else if (baseType.nodeType === "MutableRecord") {
+          baseType.fields[field.text] = type;
+        }
+      } else {
+        this.isAssignable(field, type, expected);
+      }
+    });
+
+    return baseType;
+  }
+
+  private setBinding(expr: Expression, type: Type): void {
+    const exprName = expr.text;
+    if (this.nonShadowableNames.has(exprName)) {
+      this.diagnostics.push(redefinitionError(expr));
+    }
+
+    this.nonShadowableNames.add(exprName);
+
+    this.bindings.set(expr.text, type);
+    this.expressionTypes.set(expr, type);
+  }
+
+  private bindParameters(
+    valueDeclaration: EValueDeclaration,
+  ): ParameterBindingResult {
+    const functionDeclarationLeft = mapSyntaxNodeToExpression(
+      TreeUtils.findFirstNamedChildOfType(
+        "function_declaration_left",
+        valueDeclaration,
+      ),
+    ) as EFunctionDeclarationLeft;
+
+    if (functionDeclarationLeft) {
+      return this.bindFunctionDeclarationParameters(
+        valueDeclaration,
+        functionDeclarationLeft,
+      );
+    }
+
+    if (valueDeclaration.pattern) {
+      this.bindPatternDeclarationParameters(
+        valueDeclaration,
+        valueDeclaration.pattern,
+      );
+      return { bindingType: "Other", count: 0 };
+    }
+
+    return { bindingType: "Other", count: 0 };
+  }
+
+  private bindFunctionDeclarationParameters(
+    valueDeclaration: EValueDeclaration,
+    functionDeclaration: EFunctionDeclarationLeft,
+  ): ParameterBindingResult {
+    const typeRefType = valueDeclaration.typeAnnotation
+      ? TypeExpression.typeAnnotationInference(
+          valueDeclaration.typeAnnotation,
+          this.uri,
+          this.elmWorkspace,
+          true,
+        )?.type
+      : undefined;
+
+    const patterns = functionDeclaration.params;
+
+    if (!typeRefType) {
+      const params = this.uniqueVars(patterns.length);
+      patterns.forEach((pat, i) => this.bindPattern(pat, params[i], true));
+      return {
+        bindingType: "Unannotated",
+        params,
+        count: params.length,
+      } as Unannotated;
+    }
+
+    const maxParams =
+      typeRefType.nodeType === "Function" ? typeRefType.params.length : 0;
+
+    if (patterns.length > maxParams) {
+      this.diagnostics.push(
+        parameterCountError(
+          patterns[0],
+          patterns[patterns.length - 1],
+          patterns.length,
+          maxParams,
+        ),
+      );
+      patterns.forEach((pat) => this.bindPattern(pat, TUnknown, true));
+      return { count: maxParams } as Other;
+    }
+
+    if (typeRefType.nodeType === "Function") {
+      patterns.forEach((pat, i) =>
+        this.bindPattern(pat, typeRefType.params[i], true),
+      );
+    }
+
+    this.annotationVars = allTypeVars(typeRefType);
+
+    return {
+      bindingType: "Annotated",
+      type: typeRefType,
+      count: patterns.length,
+    } as Annotated;
+  }
+
+  private bindPatternDeclarationParameters(
+    valueDeclaration: EValueDeclaration,
+    pattern: EPattern,
+  ): void {
+    // Bind all patters to a InProgressBinding type
+    // so we can use them before we know the type
+    const declaredNames = pattern.descendantsOfType("lower_pattern");
+    declaredNames.forEach((name) =>
+      this.bindings.set(name.text, TInProgressBinding),
+    );
+
+    const bodyType: Type = valueDeclaration.body
+      ? this.infer(valueDeclaration.body)
+      : TUnknown;
+    this.bindPattern(pattern, bodyType, false);
+
+    // Make sure there are none still in progress, or else there was a mistake
+    declaredNames.forEach((name) => {
+      if (
+        this.getBinding(name as Expression)?.nodeType === "InProgressBinding"
+      ) {
+        throw new Error(`Failed to bind parameter ${name.text}`);
+      }
+    });
+  }
+
+  private bindPattern(
+    pattern: Expression,
+    type: Type,
+    isParameter: boolean,
+  ): void {
+    const ty = this.replacements.get(type) ?? type;
+    switch (pattern.nodeType) {
+      case "AnythingPattern":
+        break;
+      case "ConsPattern":
+        if (isParameter) {
+          this.diagnostics.push(partialPatternError(pattern));
+          this.bindConsPattern(pattern, TUnknown);
+        } else {
+          this.bindConsPattern(pattern, ty);
+        }
+        break;
+      case "LowerPattern":
+        this.setBinding(pattern, ty);
+        break;
+      case "ListPattern":
+        if (isParameter) {
+          this.bindListPattern(pattern, TUnknown);
+        } else {
+          this.bindListPattern(pattern, ty);
+        }
+        break;
+      case "Pattern":
+        {
+          const child = mapSyntaxNodeToExpression(
+            pattern.namedChildren.find(
+              (c) =>
+                c.type.endsWith("pattern") ||
+                c.type.includes("constant") ||
+                c.type === "unit_expr",
+            ),
+          );
+          if (!child) {
+            throw new Error("Missing pattern child");
+          }
+
+          this.bindPattern(child, ty, isParameter);
+          if (pattern.patternAs) {
+            this.bindPattern(pattern.patternAs, ty, isParameter);
+          }
+        }
+        break;
+      case "RecordPattern":
+        this.bindRecordPattern(pattern, ty, isParameter);
+        break;
+      case "TuplePattern":
+        this.bindTuplePattern(pattern, ty, isParameter);
+        break;
+      case "UnionPattern":
+        this.bindUnionPattern(pattern, ty, isParameter);
+        break;
+      case "UnitExpr":
+        this.isAssignable(pattern, ty, TUnit, undefined, true);
+        break;
+      case "StringConstant":
+      case "NumberConstant":
+      case "CharConstant":
+        if (isParameter) {
+          this.diagnostics.push(partialPatternError(pattern));
+        }
+        break;
+      default:
+        throw new Error("Unexpected pattern type: " + pattern.nodeType);
+    }
+  }
+
+  private bindTuplePattern(
+    tuplePattern: ETuplePattern,
+    type: Type,
+    isParameter: boolean,
+  ): void {
+    const patterns = tuplePattern.patterns;
+    const ty = this.bindIfVar(
+      tuplePattern,
+      type,
+      TTuple(this.uniqueVars(patterns.length)),
+    );
+
+    if (ty.nodeType !== "Tuple" || ty.types.length !== patterns.length) {
+      patterns.forEach((pat) => this.bindPattern(pat, TUnknown, isParameter));
+
+      if (ty.nodeType !== "Unknown") {
+        const actualType = TTuple(this.uniqueVars(patterns.length));
+        this.diagnostics.push(
+          typeMismatchError(tuplePattern, actualType, ty, tuplePattern, true),
+        );
+      }
+      return;
+    }
+
+    patterns.forEach((pat, i) => {
+      this.bindPattern(pat, ty.types[i], isParameter);
+    });
+  }
+
+  private bindUnionPattern(
+    unionPattern: EUnionPattern,
+    type: Type,
+    isParameter: boolean,
+  ): void {
+    const variant = findDefinition(
+      unionPattern.constructor.lastNamedChild,
+      this.uri,
+      this.elmWorkspace.getImports(),
+    );
+
+    if (variant?.expr.nodeType !== "UnionVariant") {
+      throw new Error("Could not find UnionVariant for " + unionPattern.text);
+    }
+
+    const variantType = TypeExpression.unionVariantInference(
+      variant.expr,
+      variant.uri,
+      this.elmWorkspace,
+    ).type;
+
+    if (!variantType || variantType.nodeType === "Unknown") {
+      unionPattern.namedParams.forEach((p) => this.setBinding(p, TUnknown));
+      return;
+    }
+
+    const issueError = (actual: number, expected: number): void => {
+      this.diagnostics.push(
+        argumentCountError(unionPattern, unionPattern, actual, expected, true),
+      );
+      unionPattern.namedParams.forEach((p) => this.setBinding(p, TUnknown));
+    };
+
+    if (variantType.nodeType === "Function") {
+      const ty = this.bindIfVar(unionPattern, type, variantType.return);
+      if (this.isAssignable(unionPattern, ty, variantType.return)) {
+        if (unionPattern.argPatterns.length !== variantType.params.length) {
+          issueError(
+            unionPattern.argPatterns.length,
+            variantType.params.length,
+          );
+        } else {
+          unionPattern.argPatterns.forEach((p, i) => {
+            this.bindPattern(p, variantType.params[i], isParameter);
+          });
+        }
+      } else {
+        unionPattern.namedParams.forEach((p) => this.setBinding(p, TUnknown));
+      }
+    } else {
+      const ty = this.bindIfVar(unionPattern, type, variantType);
+      if (
+        this.isAssignable(unionPattern, ty, variantType) &&
+        unionPattern.argPatterns.length > 0
+      ) {
+        issueError(unionPattern.argPatterns.length, 0);
+      } else {
+        unionPattern.namedParams.forEach((p) => this.setBinding(p, TUnknown));
+      }
+    }
+  }
+
+  private bindConsPattern(consPattern: EConsPattern, type: Type): void {
+    this.bindListPatternParts(consPattern, consPattern.parts, type, true);
+  }
+
+  private bindListPattern(listPattern: EListPattern, type: Type): void {
+    this.bindListPatternParts(listPattern, listPattern.parts, type, true);
+  }
+
+  private bindListPatternParts(
+    listPattern: EListPattern | EConsPattern,
+    parts: Expression[],
+    type: Type,
+    isCons: boolean,
+  ): void {
+    const ty = this.bindIfVar(listPattern, type, TList(TVar("a")));
+
+    if (ty.nodeType !== "Union" || ty.name !== "List") {
+      if (ty.nodeType !== "Unknown") {
+        this.diagnostics.push(
+          typeMismatchError(
+            listPattern,
+            TList(TVar("a")),
+            ty,
+            listPattern,
+            true,
+          ),
+        );
+      }
+
+      parts.forEach((p) => this.bindPattern(p, TUnknown, false));
+      return;
+    }
+
+    const innerType = ty.params[0];
+
+    parts.slice(0, parts.length - 1).forEach((part) => {
+      const t = part.nodeType === "ListPattern" ? ty : innerType;
+      this.bindPattern(part, t, false);
+    });
+
+    if (parts.length > 0) {
+      this.bindPattern(parts[parts.length - 1], isCons ? ty : innerType, false);
+    }
+  }
+
+  private bindRecordPattern(
+    pattern: ERecordPattern,
+    type: Type,
+    isParameter: boolean,
+  ): void {
+    const fields = pattern.patternList;
+
+    const ty = this.bindIfVar(pattern, type, TMutableRecord({}, TVar("a")));
+
+    const vars = this.uniqueVars(fields.length);
+    if (ty.nodeType === "MutableRecord") {
+      fields.forEach((field, i) => {
+        const existing = ty.fields[field.text];
+
+        if (!existing) {
+          ty.fields[field.text] = vars[i];
+        }
+      });
+    } else if (
+      ty.nodeType !== "Record" ||
+      fields.some((field) => !Object.keys(ty.fields).includes(field.text))
+    ) {
+      if (ty.nodeType !== "Unknown") {
+        const actualTyParams = Object.fromEntries(
+          fields.map((field, i) => [field.text, vars[i]] as [string, Type]),
+        );
+        const actualTy = TRecord(actualTyParams);
+        const recordDiff: RecordDiff | undefined =
+          ty.nodeType === "Record"
+            ? {
+                ...this.calculateRecordDiff(actualTy, ty),
+                missing: new Map<string, Type>(),
+              }
+            : undefined;
+
+        this.diagnostics.push(
+          typeMismatchError(pattern, actualTy, ty, undefined, true, recordDiff),
+        );
+
+        if (recordDiff) {
+          this.recordDiffs.set(pattern, recordDiff);
+        }
+      }
+
+      fields.forEach((field) => {
+        this.bindPattern(field, TUnknown, isParameter);
+      });
+
+      return;
+    }
+
+    const tyFields = (ty as TMutableRecord)?.fields ?? (ty as TRecord).fields;
+    fields.forEach((field) => {
+      this.bindPattern(field, tyFields[field.text], isParameter);
+    });
+
+    this.expressionTypes.set(pattern, type);
+  }
+
+  private isAssignable(
+    expr: Expression,
+    type1: Type,
+    type2: Type,
+    endExpr?: Expression,
+    patternBinding = false,
+  ): boolean {
+    let assignable: boolean;
+
+    if (expr.nodeType === "CaseOfExpr") {
+      return this.isBranchesAssignable(expr, type1, type2);
+    }
+
+    try {
+      assignable = this.assignable(type1, type2);
+    } catch (e) {
+      this.diagnostics.push({
+        node: expr,
+        endNode: endExpr ?? expr,
+        message: e,
+      });
+      return false;
+    }
+
+    if (!assignable) {
+      const t1 = TypeReplacement.replace(type1, this.replacements.toMap());
+      const t2 = TypeReplacement.replace(type2, this.replacements.toMap());
+
+      const diff =
+        t1.nodeType === "Record" && t2.nodeType === "Record"
+          ? this.calculateRecordDiff(t1, t2)
+          : undefined;
+
+      const errorExpr = expr.nodeType === "LetInExpr" ? expr.body : expr;
+      this.diagnostics.push(
+        typeMismatchError(errorExpr, t1, t2, errorExpr, patternBinding),
+      );
+
+      if (diff && expr.nodeType === "RecordExpr") {
+        this.recordDiffs.set(expr, diff);
+      }
+    }
+
+    return assignable;
+  }
+
+  private isBranchesAssignable(
+    expr: ECaseOfExpr,
+    type1: Type,
+    type2: Type,
+  ): boolean {
+    // If type2 is not a concrete type, then every branch should match the first type
+    const t2 =
+      type2.nodeType !== "Unknown" && type2.nodeType !== "Var" ? type2 : type1;
+
+    return expr.branches.every((branch) => {
+      const t1 = this.expressionTypes.get(branch.expr);
+      return t1 ? this.isAssignable(branch.expr, t1, t2) : false;
+    });
+  }
+
+  private assignable(type1: Type, type2: Type): boolean {
+    const ty1 = this.replacements.get(type1);
+    const ty2 = this.replacements.get(type2);
+
+    let result =
+      ty1 === ty2 || ty1?.nodeType === "Unknown" || ty2?.nodeType === "Unknown";
+
+    if (!result) {
+      if (ty1 && ty2 && ty1.nodeType !== "Var" && ty2.nodeType === "Var") {
+        result = this.nonVarAssignableToVar(ty1, ty2);
+      } else {
+        switch (ty1?.nodeType) {
+          case "Var":
+            if (ty2?.nodeType === "Var") {
+              result = this.varsAssignable(ty1, ty2);
+            } else if (ty2) {
+              result = this.nonVarAssignableToVar(ty2, ty1);
+            }
+            break;
+          case "Union":
+            {
+              result =
+                ty2?.nodeType === "Union" &&
+                ty1.name === ty2.name &&
+                ty1.module === ty2.module &&
+                this.allAssignable(ty1.params, ty2.params);
+            }
+            break;
+          case "Function":
+            {
+              result =
+                ty2?.nodeType === "Function" &&
+                this.functionAssignable(ty1, ty2);
+            }
+            break;
+          case "Tuple":
+            {
+              result =
+                ty2?.nodeType === "Tuple" &&
+                ty1.types.length === ty2.types.length &&
+                this.allAssignable(ty1.types, ty2.types);
+            }
+            break;
+          case "Record":
+            {
+              result =
+                (ty2?.nodeType === "Record" &&
+                  this.recordAssignable(ty1, ty2)) ||
+                (ty2?.nodeType === "MutableRecord" &&
+                  this.mutableRecordAssignable(ty2, ty1));
+            }
+            break;
+          case "MutableRecord":
+            {
+              result =
+                (ty2?.nodeType === "Record" &&
+                  this.mutableRecordAssignable(ty1, ty2)) ||
+                (ty2?.nodeType === "MutableRecord" &&
+                  this.mutableRecordAssignable(ty1, ty2));
+            }
+            break;
+          case "Unit":
+            result = ty2?.nodeType === "Unit";
+            break;
+          case "Unknown":
+            result = true;
+            break;
+          case "InProgressBinding":
+            throw new Error(
+              `Should never try to assign an in progress binding`,
+            );
+        }
+      }
+    }
+
+    if (result && ty1 && ty2) {
+      this.trackReplacement(ty1, ty2);
+    }
+    return result;
+  }
+
+  private mutableRecordAssignable(
+    type1: TMutableRecord,
+    type2: TRecord | TMutableRecord,
+  ): boolean {
+    if (!this.recordAssignable(type1, type2)) {
+      return false;
+    }
+
+    type1.fields = { ...type1.fields, ...type2.fields };
+    return true;
+  }
+
+  private recordAssignable(
+    type1: TRecord | TMutableRecord,
+    type2: TRecord | TMutableRecord,
+  ): boolean {
+    const result = this.calculateRecordDiff(type1, type2).isEmpty;
+    if (result) {
+      if (!type1.baseType && type2.baseType?.nodeType === "Var") {
+        this.trackReplacement(type1, type2.baseType);
+      }
+      if (!type2.baseType && type1.baseType?.nodeType === "Var") {
+        this.trackReplacement(type1.baseType, type2);
+      }
+
+      type1.fieldReferences.addAll(type2.fieldReferences);
+      type2.fieldReferences.addAll(type1.fieldReferences);
+    }
+
+    return result;
+  }
+
+  private calculateRecordDiff(
+    actual: TRecord | TMutableRecord,
+    expected: TRecord | TMutableRecord,
+  ): RecordDiff {
+    const actualEntries = Object.entries(actual.fields);
+    const actualKeys = Object.keys(actual.fields);
+    const expectedEntries = Object.entries(expected.fields);
+    const expectedKeys = Object.keys(expected.fields);
+
+    return RecordDiff(
+      expected.baseType
+        ? new Map<string, Type>()
+        : new Map(
+            actualEntries.filter(([field]) => !expectedKeys.includes(field)),
+          ),
+      actual.baseType
+        ? new Map()
+        : new Map(
+            expectedEntries.filter(([field]) => !actualKeys.includes(field)),
+          ),
+      new Map(
+        actualEntries
+          .map(([k, v]) => {
+            if (expected.fields[k] && !this.assignable(v, expected.fields[k])) {
+              return [k, [v, expected.fields[k]]] as [string, [Type, Type]];
+            }
+
+            return undefined;
+          })
+          .filter(Utils.notUndefined.bind(this)),
+      ),
+    );
+  }
+
+  private allAssignable(type1: Type[], type2: Type[]): boolean {
+    return (
+      type1.length === type2.length &&
+      type1.every((t, i) => this.assignable(t, type2[i]))
+    );
+  }
+
+  private functionAssignable(type1: TFunction, type2: TFunction): boolean {
+    const allTypes1 = type1.params.concat(type1.return);
+    const allTypes2 = type2.params.concat(type2.return);
+
+    // If there is only one type, return it
+    // If there are multiple, curry it into a new function
+    function makeFunction(types: Type[]): Type {
+      if (types.length === 1) {
+        return types[0];
+      } else {
+        return TFunction(
+          types.slice(0, types.length - 2),
+          types[types.length - 1],
+        );
+      }
+    }
+
+    const paramsSize = Math.min(allTypes1.length, allTypes2.length) - 1;
+
+    // Make sure the shared parameters are assignable
+    const sharedAssignable = this.allAssignable(
+      allTypes1.slice(0, paramsSize),
+      allTypes2.slice(0, paramsSize),
+    );
+
+    const tailAssignable = this.assignable(
+      makeFunction(allTypes1.slice(paramsSize)),
+      makeFunction(allTypes2.slice(paramsSize)),
+    );
+
+    return sharedAssignable && tailAssignable;
+  }
+
+  /**
+   * Check that two type vars can be unified
+   */
+  private varsAssignable(type1: TVar, type2: TVar): boolean {
+    const typeClass1 = getTypeclassName(type1);
+    const typeClass2 = getTypeclassName(type2);
+
+    if (!type1.rigid && !typeClass1) {
+      return true;
+    } else if (!type1.rigid && typeClass1) {
+      return (
+        this.typeclassesCompatible(typeClass1, typeClass2, !type2.rigid) ||
+        (!type2.rigid &&
+          this.typeclassesConstrainToCompappend(typeClass1, typeClass2))
+      );
+    } else if (type1.rigid && !typeClass1) {
+      return !type2.rigid && !typeClass2;
+    } else if (type1.rigid && typeClass1 && type2.rigid) {
+      // If they are both rigid and we have a type class, they must be the same typeclass
+      return typeClass1 === typeClass2;
+    } else if (type1.rigid && typeClass1 && !type2.rigid) {
+      return this.typeclassesCompatible(typeClass1, typeClass2, !type2.rigid);
+    } else {
+      throw new Error("Impossible");
+    }
+  }
+
+  private nonVarAssignableToVar(type: Type, typeVar: TVar): boolean {
+    const allAssignableTo = (types: Type[], typeClass: string): boolean => {
+      return types.every((t) => this.assignable(t, TVar(typeClass)));
+    };
+
+    if (typeVar.name.startsWith("number")) {
+      return (
+        type.nodeType === "Union" &&
+        (type.name === "Float" || type.name === "Int")
+      );
+    } else if (typeVar.name.startsWith("appendable")) {
+      return (
+        type.nodeType === "Union" &&
+        (type.name === "String" || type.name === "List")
+      );
+    } else if (typeVar.name.startsWith("comparable")) {
+      if (type.nodeType === "Tuple") {
+        return allAssignableTo(type.types, "comparable");
+      } else if (type.nodeType === "Union") {
+        return (
+          type.name === "Float" ||
+          type.name === "Int" ||
+          type.name === "Char" ||
+          type.name === "String" ||
+          (type.name === "List" &&
+            (allAssignableTo(type.params, "comparable") ||
+              allAssignableTo(type.params, "number")))
+        );
+      } else {
+        return false;
+      }
+    } else if (typeVar.name.startsWith("compappend")) {
+      return (
+        type.nodeType === "Union" &&
+        (type.name === "String" ||
+          (type.name === "List" &&
+            (allAssignableTo(type.params, "comparable") ||
+              allAssignableTo(type.params, "compappend"))))
+      );
+    }
+
+    return !typeVar.rigid;
+  }
+
+  private typeclassesCompatible(
+    name1: string,
+    name2?: string,
+    unconstrainedAllowed = true,
+  ): boolean {
+    if (!name2) {
+      return unconstrainedAllowed;
+    }
+    if (name1 === name2) {
+      return true;
+    }
+    if (name1 === "number" && name2 === "comparable") {
+      return true;
+    }
+    if (name1 === "comparable" && name2 === "number") {
+      return true;
+    }
+    if (
+      name1 === "comparable" &&
+      (name2 === "number" || name2 === "compappend")
+    ) {
+      return true;
+    }
+    if (
+      name1 === "compappend" &&
+      (name2 === "comparable" || name2 === "appendable")
+    ) {
+      return true;
+    }
+    return false;
+  }
+
+  private typeclassesConstrainToCompappend(
+    tc1?: string,
+    tc2?: string,
+  ): boolean {
+    if (tc1 === "comparable") {
+      return tc2 === "appendable" || tc2 === "compappend";
+    }
+    if (tc1 === "appendable") {
+      return tc2 === "comparable" || tc2 === "compappend";
+    }
+    return false;
+  }
+
+  private trackReplacement(type1: Type, type2: Type): void {
+    if (type1 === type2) {
+      return;
+    }
+
+    // Assign k to be of type v
+    const assign = (typeVar: TVar, type: Type): void => {
+      if (anyTypeVar(type, (tVar) => tVar === typeVar)) {
+        throw Error("InfiniteTypeException");
+      }
+      this.replacements.set(typeVar, type);
+    };
+
+    // If we assign anything to a var, the type is constrained to that var
+    if (
+      type2.nodeType === "Var" &&
+      (!this.replacements.contains(type2) ||
+        (type1.nodeType !== "Var" &&
+          this.replacements.get(type2)?.nodeType === "Var"))
+    ) {
+      if (type1.nodeType === "Var") {
+        const typeClass1 = getTypeclassName(type1);
+        const typeClass2 = getTypeclassName(type2);
+
+        if (!typeClass1 && typeClass2) {
+          // Assigning a => number, a should be constrained to number
+          assign(type1, type2);
+        } else if (
+          !type1.rigid &&
+          !type2.rigid &&
+          this.typeclassesConstrainToCompappend(typeClass1, typeClass2)
+        ) {
+          assign(
+            type1,
+            typeClass2 === "compappend" ? type2 : TVar("compappend"),
+          );
+        } else if (
+          !type1.rigid &&
+          !type2.rigid &&
+          typeClass1 === "comparable" &&
+          typeClass2 === "number"
+        ) {
+          // comparable is constrained to number
+          assign(type1, type2);
+        } else if (!type1.rigid && type2.rigid) {
+          // Assigning a flex var to a rigid var makes it rigid
+          assign(type1, type2);
+        } else {
+          assign(type2, type1);
+        }
+      } else {
+        // Int => number contrains number to be an Int
+        assign(type2, type1);
+      }
+    }
+
+    // Assigning a var to a non var type constrains the type
+    if (
+      type1.nodeType === "Var" &&
+      type2.nodeType !== "Var" &&
+      !this.replacements.contains(type1)
+    ) {
+      // TODO: Handle extension records
+      assign(type1, type2);
+    }
+  }
+
+  private bindIfVar(e: Expression, type: Type, defaultType: Type): Type {
+    if (type.nodeType === "Var") {
+      this.isAssignable(e, type, defaultType, undefined, true);
+      return defaultType;
+    } else {
+      return type;
+    }
+  }
+
+  private uniqueVars(count: number): TVar[] {
+    return getVarNames(count).map((val) => {
+      return TVar(val);
+    });
+  }
+}
+
+type BindingType = "Annotated" | "Unannotated" | "Other";
+interface ParameterBindingResult {
+  bindingType: BindingType;
+  count: number;
+}
+
+interface Annotated extends ParameterBindingResult {
+  bindingType: "Annotated";
+  type: Type;
+}
+interface Unannotated extends ParameterBindingResult {
+  bindingType: "Unannotated";
+  params: Type[];
+}
+interface Other extends ParameterBindingResult {
+  bindingType: "Other";
+}
+
+export function findType(
+  node: SyntaxNode,
+  uri: string,
+  workspace: IElmWorkspace,
+): Type {
+  let declaration: SyntaxNode | null = node;
+  while (
+    declaration &&
+    declaration.type !== "value_declaration" &&
+    declaration.parent?.type !== "file"
+  ) {
+    declaration = declaration.parent;
+  }
+
+  // We can't find the top level declaration
+  if (
+    declaration?.type !== "value_declaration" ||
+    declaration.parent?.type !== "file"
+  ) {
+    return TUnknown;
+  }
+
+  const mappedDeclaration = mapSyntaxNodeToExpression(declaration);
+
+  if (mappedDeclaration && mappedDeclaration.nodeType === "ValueDeclaration") {
+    const inferenceResult = InferenceScope.valueDeclarationInference(
+      mappedDeclaration,
+      uri,
+      workspace,
+      new Set<EValueDeclaration>(),
+    );
+
+    if (node.parent?.type === "function_declaration_left") {
+      return inferenceResult.type;
+    }
+
+    const findTypeOrParentType = (
+      expr: SyntaxNode | undefined,
+    ): Type | undefined => {
+      const found = expr
+        ? inferenceResult.expressionTypes.get(expr as Expression)
+        : undefined;
+
+      if (found) {
+        return found;
+      }
+
+      // Check if the parent is the same text and position
+      if (
+        expr &&
+        expr.text === expr.parent?.text &&
+        expr.startIndex === expr.parent?.startIndex &&
+        expr.endIndex === expr.parent?.endIndex
+      ) {
+        return findTypeOrParentType(mapSyntaxNodeToExpression(expr.parent));
+      }
+    };
+
+    return findTypeOrParentType(node) ?? TUnknown;
+  } else {
+    return TUnknown;
+  }
+}
diff --git a/src/util/types/typeRenderer.ts b/src/util/types/typeRenderer.ts
new file mode 100644
index 0000000..853f32d
--- /dev/null
+++ b/src/util/types/typeRenderer.ts
@@ -0,0 +1,118 @@
+import { IImports } from "src/imports";
+import { Tree } from "web-tree-sitter";
+import { TreeUtils } from "../treeUtils";
+import {
+  getTypeclassName,
+  getVarNames,
+  TUnion,
+  TVar,
+  Type,
+} from "./typeInference";
+
+export class TypeRenderer {
+  private usedVarNames = new Map<TVar, string>();
+
+  constructor(
+    private tree?: Tree,
+    private uri?: string,
+    private imports?: IImports,
+  ) {}
+
+  public static typeToString(
+    t: Type,
+    tree?: Tree,
+    uri?: string,
+    imports?: IImports,
+  ): string {
+    return new TypeRenderer(tree, uri, imports).render(t);
+  }
+
+  private render(t: Type): string {
+    if (t.alias) {
+      return this.renderUnion(
+        TUnion(t.alias.module, t.alias.name, t.alias.parameters),
+      );
+    }
+
+    switch (t.nodeType) {
+      case "Unknown":
+        return "Unknown";
+      case "InProgressBinding":
+        throw new Error(
+          "Should never try to convert an in progress binding type to a string",
+        );
+      case "Unit":
+        return "()";
+      case "Var":
+        return this.renderVar(t);
+      case "Function":
+        return `${[...t.params, t.return]
+          .map((p) =>
+            p.nodeType === "Function" ? `(${this.render(p)})` : this.render(p),
+          )
+          .join(" -> ")}`;
+      case "Tuple":
+        return `(${t.types.map(this.render.bind(this)).join(", ")})`;
+      case "Union":
+        return this.renderUnion(t);
+      case "Record":
+      case "MutableRecord":
+        return `{ ${
+          t.baseType ? `${this.render(t.baseType)} | ` : ""
+        }${Object.entries(t.fields)
+          .map(([field, type]) => `${field} : ${this.render(type)}`)
+          .join(", ")} }`;
+    }
+  }
+
+  private renderUnion(t: TUnion): string {
+    let type;
+    if (t.params.length === 0) {
+      type = t.name;
+    } else {
+      type = `${t.name} ${t.params
+        .map((p) =>
+          p.nodeType === "Function" ||
+          (p.nodeType === "Union" && p.params.length > 0) ||
+          (p.alias?.parameters.length ?? 0 > 0)
+            ? `(${this.render(p)})`
+            : this.render(p),
+        )
+        .join(" ")}`;
+    }
+
+    if (this.tree && this.uri && this.imports) {
+      return `${
+        TreeUtils.getQualifierForName(
+          this.tree,
+          this.uri,
+          t.module,
+          t.name,
+          this.imports,
+        ) ?? ""
+      }${type}`;
+    } else {
+      return type;
+    }
+  }
+
+  private renderVar(t: TVar): string {
+    if (this.usedVarNames.has(t)) {
+      return this.usedVarNames.get(t) ?? "";
+    }
+
+    const takenNames = Array.from(this.usedVarNames.values());
+
+    if (!getTypeclassName(t) && takenNames.includes(t.name)) {
+      const displayName =
+        getVarNames(takenNames.length + 1).find(
+          (name) => !takenNames.includes(name),
+        ) ?? "";
+      this.usedVarNames.set(t, displayName);
+      return displayName;
+    }
+
+    this.usedVarNames.set(t, t.name);
+    return t.name;
+  }
+}
diff --git a/src/util/types/typeReplacement.ts b/src/util/types/typeReplacement.ts
new file mode 100644
index 0000000..01a699a
--- /dev/null
+++ b/src/util/types/typeReplacement.ts
@@ -0,0 +1,248 @@
+import { RecordFieldReferenceTable } from "./recordFieldReferenceTable";
+import {
+  TVar,
+  Type,
+  Alias,
+  TFunction,
+  uncurryFunction,
+  TUnion,
+  TTuple,
+  TRecord,
+  TMutableRecord,
+} from "./typeInference";
+
+export class TypeReplacement {
+  private replacements: Map<TVar, [boolean, Type]>;
+
+  constructor(
+    replacements: Map<TVar, Type>,
+    private freshen: boolean,
+    private keepRecordsMutable: boolean,
+    private varsToRemainRigid?: TVar[],
+  ) {
+    this.replacements = new Map<TVar, [boolean, Type]>();
+    replacements.forEach((value, key) => {
+      this.replacements.set(key, [false, value]);
+    });
+  }
+
+  public static replace(
+    type: Type,
+    replacements: Map<TVar, Type>,
+    keepRecordsMutable = false,
+    varsToRemainRigid?: TVar[],
+  ): Type {
+    if (!varsToRemainRigid && replacements.size === 0) {
+      return type;
+    }
+
+    return new TypeReplacement(
+      replacements,
+      false,
+      keepRecordsMutable,
+      varsToRemainRigid,
+    ).replace(type);
+  }
+
+  public static freshenVars(type: Type): Type {
+    return new TypeReplacement(new Map(), true, false).replace(type);
+  }
+
+  public static flexify(type: Type): Type {
+    return new TypeReplacement(new Map(), false, false, []).replace(type);
+  }
+
+  public static freeze(type: Type): void {
+    switch (type.nodeType) {
+      case "Tuple":
+        type.types.forEach(this.freeze.bind(this));
+        break;
+      case "Record":
+      case "MutableRecord":
+        (type.baseType as TRecord)?.fieldReferences?.freeze();
+        for (const field in type.fields) {
+          this.freeze(type.fields[field]);
+        }
+        type.fieldReferences.freeze();
+        break;
+      case "Union":
+        type.params.forEach(this.freeze.bind(this));
+        break;
+      case "Function":
+        this.freeze(type.return);
+        type.params.forEach(this.freeze.bind(this));
+        break;
+    }
+
+    type.alias?.parameters.forEach(TypeReplacement.freeze.bind(this));
+  }
+
+  private replace(type: Type): Type {
+    switch (type.nodeType) {
+      case "Var":
+        return this.getReplacement(type) ?? type;
+      case "Function":
+        return this.replaceFunction(type);
+      case "Union":
+        return this.replaceUnion(type);
+      case "Tuple":
+        return this.replaceTuple(type);
+      case "Record":
+        return this.replaceRecord(
+          type.fields,
+          type.fieldReferences,
+          false,
+          type.baseType,
+          type.alias,
+        );
+      case "MutableRecord":
+        return this.replaceRecord(
+          type.fields,
+          type.fieldReferences,
+          true,
+          type.baseType,
+        );
+      case "Unit":
+      case "InProgressBinding":
+        return type;
+      case "Unknown":
+        return {
+          nodeType: "Unknown",
+          alias: this.replaceAlias(type.alias),
+        };
+    }
+  }
+
+  private replaceAlias(alias?: Alias): Alias | undefined {
+    if (alias) {
+      return {
+        ...alias,
+        parameters: alias.parameters.map((param) => this.replace(param)),
+      };
+    }
+  }
+
+  private replaceTuple(type: TTuple): TTuple {
+    return TTuple(
+      type.types.map((t) => this.replace(t)),
+      this.replaceAlias(type.alias),
+    );
+  }
+
+  private replaceFunction(type: TFunction): TFunction {
+    const params = type.params.map((param) => this.replace(param));
+    return uncurryFunction(
+      TFunction(
+        params,
+        this.replace(type.return),
+        this.replaceAlias(type.alias),
+      ),
+    );
+  }
+
+  private replaceUnion(type: TUnion): TUnion {
+    if (type.params.length === 0 && !type.alias) {
+      return type;
+    }
+
+    const params = type.params.map((param) => this.replace(param));
+    return TUnion(
+      type.module,
+      type.name,
+      params,
+      this.replaceAlias(type.alias),
+    );
+  }
+
+  private replaceRecord(
+    fields: { [key: string]: Type },
+    fieldReferences: RecordFieldReferenceTable,
+    wasMutable: boolean,
+    baseType?: Type,
+    alias?: Alias,
+  ): Type {
+    const oldBase =
+      !baseType || baseType.nodeType !== "Var"
+        ? undefined
+        : this.getReplacement(baseType);
+
+    let newBase = oldBase;
+
+    if (oldBase?.nodeType === "Record") {
+      newBase = oldBase.baseType;
+    } else if (!oldBase) {
+      if (baseType?.nodeType === "MutableRecord" && !this.keepRecordsMutable) {
+        newBase = this.replace(baseType);
+      } else {
+        newBase = baseType;
+      }
+    }
+
+    const baseFields = (oldBase as TRecord)?.fields ?? [];
+    const baseFieldRefs = (oldBase as TRecord)?.fieldReferences;
+
+    const newFields: { [key: string]: Type } = {};
+
+    for (const field in baseFields) {
+      newFields[field] = baseFields[field];
+    }
+
+    for (const field in fields) {
+      newFields[field] = this.replace(fields[field]);
+    }
+
+    let newFieldReferences: RecordFieldReferenceTable;
+
+    if (!baseFieldRefs || baseFieldRefs.isEmpty()) {
+      newFieldReferences = fieldReferences;
+    } else if (fieldReferences.frozen) {
+      newFieldReferences = fieldReferences.plus(baseFieldRefs);
+    } else {
+      fieldReferences.addAll(baseFieldRefs);
+      newFieldReferences = fieldReferences;
+    }
+
+    if (wasMutable && this.keepRecordsMutable) {
+      return TMutableRecord(newFields, newBase, newFieldReferences);
+    } else {
+      return TRecord(
+        newFields,
+        newBase,
+        this.replaceAlias(alias),
+        newFieldReferences,
+      );
+    }
+  }
+
+  private getReplacement(key: TVar): Type | undefined {
+    const replacement = this.replacements.get(key);
+
+    if (!replacement) {
+      if (this.freshen || this.varsToRemainRigid) {
+        if (
+          key.rigid &&
+          (!this.varsToRemainRigid || this.varsToRemainRigid.includes(key))
+        ) {
+          return undefined;
+        } else {
+          const newVar = TVar(key.name);
+          this.replacements.set(key, [true, newVar]);
+          return newVar;
+        }
+      }
+
+      return undefined;
+    }
+
+    const hasBeenAccessed = replacement[0];
+    const storedType = replacement[1];
+
+    if (hasBeenAccessed) {
+      return storedType;
+    }
+
+    const replacedType = this.replace(storedType);
+    this.replacements.set(key, [true, replacedType]);
+    return replacedType;
+  }
+}
diff --git a/src/util/utils.ts b/src/util/utils.ts
index d594277..96d43f8 100644
--- a/src/util/utils.ts
+++ b/src/util/utils.ts
@@ -2,4 +2,8 @@ export class Utils {
   public static notUndefined<T>(x: T | undefined): x is T {
     return x !== undefined;
   }
+
+  public static notUndefinedOrNull<T>(x: T | undefined | null): x is T {
+    return x !== undefined && x !== null;
+  }
 }


Test patch:
diff --git a/test/definitionProviderTests/importDefinition.test.ts b/test/definitionProviderTests/importDefinition.test.ts
index 45aea92..cec668a 100644
--- a/test/definitionProviderTests/importDefinition.test.ts
+++ b/test/definitionProviderTests/importDefinition.test.ts
@@ -389,7 +389,7 @@ module Data.User exposing (..)
     await testBase.testDefinition(source);
   });
 
-  xit(`test port ref`, async () => {
+  it(`test port ref`, async () => {
     const source = `
 --@ main.elm
 import Ports exposing (foo)
diff --git a/test/definitionProviderTests/miscDefinition.test.ts b/test/definitionProviderTests/miscDefinition.test.ts
index 45dbdce..dbd74e4 100644
--- a/test/definitionProviderTests/miscDefinition.test.ts
+++ b/test/definitionProviderTests/miscDefinition.test.ts
@@ -102,11 +102,11 @@ f = \((x,y) as point) -> point
 
   // PORTS
 
-  xit(`test port ref`, async () => {
+  it(`test port ref`, async () => {
     const source = `
 port module Ports exposing (..)
 port foo : String -> Cmd msg
-     --X
+--X
 update msg model = (model, foo "blah")
                            --^
 `;
diff --git a/test/definitionProviderTests/patternDefinition.test.ts b/test/definitionProviderTests/patternDefinition.test.ts
index 29dfe25..b02ed08 100644
--- a/test/definitionProviderTests/patternDefinition.test.ts
+++ b/test/definitionProviderTests/patternDefinition.test.ts
@@ -27,7 +27,7 @@ f x =
     await testBase.testDefinition(source);
   });
 
-  xit(`test case-of pattern union type constructor with constructor parameter`, async () => {
+  it(`test case-of pattern union type constructor with constructor parameter`, async () => {
     const source = `
 type Foo = Foo
            --X
diff --git a/test/definitionProviderTests/qualifiedImportDefinition.test.ts b/test/definitionProviderTests/qualifiedImportDefinition.test.ts
index 8ba0c08..18e91a7 100644
--- a/test/definitionProviderTests/qualifiedImportDefinition.test.ts
+++ b/test/definitionProviderTests/qualifiedImportDefinition.test.ts
@@ -96,7 +96,7 @@ type alias Person = { name : String, age: Int }
     await testBase.testDefinition(source);
   });
 
-  xit(`test qualified port ref`, async () => {
+  it(`test qualified port ref`, async () => {
     const source = `
 --@ main.elm
 import Ports
diff --git a/test/typeInference.test.ts b/test/typeInference.test.ts
new file mode 100644
index 0000000..1e606dc
--- /dev/null
+++ b/test/typeInference.test.ts
@@ -0,0 +1,149 @@
+import { URI } from "vscode-uri";
+import { TreeUtils } from "../src/util/treeUtils";
+import { findType } from "../src/util/types/typeInference";
+import { TypeRenderer } from "../src/util/types/typeRenderer";
+import { baseUri } from "./utils/mockElmWorkspace";
+import { getTargetPositionFromSource } from "./utils/sourceParser";
+import { SourceTreeParser } from "./utils/sourceTreeParser";
+
+const basicsSources = `
+--@ Basics.elm
+module Basics exposing (add, (+), Int, Float, Bool(..))
+
+infix left  6 (+)  = add
+
+type Int = Int
+
+type Float = Float
+
+type Bool = True | False
+
+add : number -> number -> number
+add =
+  Elm.Kernel.Basics.add
+
+`;
+describe("test type inference", () => {
+  const treeParser = new SourceTreeParser();
+
+  async function testTypeInference(source: string, expectedType: string) {
+    await treeParser.init();
+
+    const result = getTargetPositionFromSource(source);
+
+    if (!result) {
+      throw new Error("Getting source and target position failed");
+    }
+
+    const testUri = URI.file(baseUri + "Test.elm").toString();
+
+    const workspace = treeParser.getWorkspace(result.sources);
+    const tree = workspace.getForest().getByUri(testUri)?.tree;
+
+    if (!tree) throw new Error("Getting tree failed");
+
+    const nodeAtPosition = TreeUtils.getNamedDescendantForPosition(
+      tree.rootNode,
+      result.position,
+    );
+
+    const nodeType = findType(nodeAtPosition, testUri, workspace);
+
+    expect(TypeRenderer.typeToString(nodeType, tree)).toEqual(expectedType);
+  }
+
+  test("simple function", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+func = 5 + 6
+--^
+`;
+    await testTypeInference(basicsSources + source, "number");
+  });
+
+  test("simple function with params", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+add a b = a + b
+
+test c d = 1 + (add c d)
+--^
+`;
+
+    await testTypeInference(
+      basicsSources + source,
+      "number -> number -> number",
+    );
+  });
+
+  test("simple int", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+func = 0
+--^
+`;
+    await testTypeInference(basicsSources + source, "number");
+  });
+
+  test("simple string", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+func = "bla"
+--^
+  `;
+    await testTypeInference(basicsSources + source, "String");
+  });
+
+  test("simple bool", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+func = True
+--^
+`;
+    await testTypeInference(basicsSources + source, "Bool");
+  });
+
+  test("complex function", async () => {
+    const source = `
+--@ Test.elm
+module Test exposing (..)
+
+type Maybe a = Just a | Nothing
+
+add a b = a + b
+
+func a b c =
+--^
+  let
+    result = a + 1
+
+  in
+    (add result b.first.second) +
+      (if a == 1 then
+        0
+      else
+        1) +
+      (case c of
+        Just value ->
+          value
+
+        Nothing ->
+          0)
+
+`;
+    await testTypeInference(
+      basicsSources + source,
+      "number -> { a | first : { b | second : number } } -> Maybe number -> number",
+    );
+  });
+});
diff --git a/test/utils/mockElmWorkspace.ts b/test/utils/mockElmWorkspace.ts
index 680faef..e68cc47 100644
--- a/test/utils/mockElmWorkspace.ts
+++ b/test/utils/mockElmWorkspace.ts
@@ -5,13 +5,15 @@ import { IElmWorkspace } from "../../src/elmWorkspace";
 import { Forest, IForest } from "../../src/forest";
 import { Imports } from "../../src/imports";
 import { container } from "tsyringe";
+import { TypeCache } from "../../src/util/types/typeCache";
 
 export const baseUri = Path.join(__dirname, "../sources/src/");
 
 export class MockElmWorkspace implements IElmWorkspace {
-  private imports!: Imports;
+  private imports: Imports;
   private forest: IForest = new Forest();
   private parser: Parser;
+  private typeCache = new TypeCache();
 
   constructor(sources: { [K: string]: string }) {
     this.parser = container.resolve("Parser");
@@ -67,6 +69,10 @@ export class MockElmWorkspace implements IElmWorkspace {
     return URI.file(Path.join(__dirname, "sources"));
   }
 
+  getTypeCache(): TypeCache {
+    return this.typeCache;
+  }
+
   private parseAndAddToForest(fileName: string, source: string): void {
     const tree: Tree | undefined = this.parser.parse(source);
     this.forest.setTree(
diff --git a/test/utils/sourceParser.ts b/test/utils/sourceParser.ts
index 09af05f..be162c0 100644
--- a/test/utils/sourceParser.ts
+++ b/test/utils/sourceParser.ts
@@ -142,6 +142,34 @@ export function getInvokeAndTargetPositionFromSource(source: string): TestType {
   }
 }
 
+export function getTargetPositionFromSource(
+  source: string,
+): { position: Position; sources: { [K: string]: string } } | undefined {
+  const sources = getSourceFiles(source);
+
+  let position: Position | undefined;
+
+  for (const fileName in sources) {
+    sources[fileName].split("\n").forEach((s, line) => {
+      const invokeCharacter = s.search(/--(\^)/);
+
+      if (invokeCharacter >= 0) {
+        position = {
+          line: line - 1,
+          character: invokeCharacter + 2,
+        };
+      }
+    });
+  }
+
+  if (position) {
+    return {
+      position,
+      sources,
+    };
+  }
+}
+
 function getSourceFiles(source: string): { [K: string]: string } {
   const sources: { [K: string]: string } = {};
   let currentFile = "";