{"slug": "rewriting-the-futhark-type-checker", "title": "Rewriting the Futhark type checker", "summary": "The Futhark programming language's type checker is undergoing a major refactoring, as detailed in a post about the evolution of its type system from a simple first-order monomorphic checker to one supporting higher-order functions, Hindley-Milner inference, records, uniqueness types, and size types, with the new implementation moving to a constraint-based approach to handle complexity.", "body_md": "[Rewriting the Futhark type checker](#)\n\nThis post is about the evolution of Futhark’s type checker, motivated by a\n[large refactoring I am about to\nmerge](https://github.com/diku-dk/futhark/pull/2302). It is probably mostly of\ninterest to other language designers, and contains some lessons I wish I had\nknown when we first got started - although I am not particularly well-read in\nthe type checking literature, so it’s possible all of this is old hat.\n\n[Background](#background)\n\nFar back [in the primordial\nera](2017-12-27-reflections-on-a-phd-accidentally-spent-on-language-design.html)\nwhen we first designed Futhark, the type system was simple: The only types were\nscalars, arrays, and tuples, all functions were first-order and monomorphic, and\nthere was no type inference. All functions had to be annotated with their\nparameter and return types. The only fancy feature that was present from the\n(near) beginning was [in-place updates via a kind of uniqueness\ntypes](2022-06-13-uniqueness-types.html), although at the time we didn’t really\nunderstand how tricky they were.\n\n[At that\npoint](https://github.com/diku-dk/futhark/blob/48ca2dedf23990e2b0ffd00a26ab32ca4f0ef22c/src/L0/TypeChecker.hs)\nit really was just a type *checker*: starting from the facts that we were told\n(the types of parameters), we checked if the types of expressions were\nconsistent, and tracked aliases to validate the use of in-place updates. The\nimplementation was a straightforward top-down single traversal of each function\nin turn, during which AST were decorated nodes with their determined type, which\nwas then used by subsequent passes. It was the exact same kind of type checker\nthat I had been taught (and since taught myself) in a standard course on\nprogramming language implementation, and when you can get away with it, this is\na *good* design: easy to implement, pretty efficient, easy to understand. At the\ntime we really didn’t understand the complexity of *correctly* checking the\nrules for safe use of in-place updates, so the simplicity was perhaps illusory,\nbut we were happy in those days nonetheless.\n\nOf course, we didn’t get away with it for long. We eventually started adding\nfeatures such as [records](2017-03-06-futhark-record-system.html) and an\n[ML-style module system](2017-01-25-futhark-module-system.html), and by version\n0.4.0 [we had put in higher-order functions and Hindley-Milner style type\ninference](2018-04-10-futhark-0.4.0-released.html). The implementation was a\nclassic (some might say “old-fashioned”) implementation of [Algorithm\nW](https://jeremymikkola.com/posts/2018_03_25_understanding_algorithm_w.html);\nthe standard Hindley-Milner type inference algorithm. During traversal of a\nfunction, we immediately solved type constraints. However, we had some type\nsystem features that were never a natural fit with this approach. Both our\nrecord system, and also our uniqueness types, depended on being able to query\nthe current type of some program fragment and make decisions based on it. This\nwould show up in cases like branches, where the aliases of `if a then b else c`\n\ndepended on whether the type of `b`\n\nand `c`\n\ncan carry aliases (primitives\ncannot). We managed to put together an implementation that worked by putting\nvarious extra information into our representation of type variables, but it was\na little complicated, not obviously correct, and somewhat tedious to debug.\n\nLater, when we added [proper size types](2019-08-03-towards-size-types.html), we\ndid it by extending the basic Hindley-Milner system by essentially also treating\nsizes as a kind of “type” subject to unification. At this point the system was\nbeginning to creak. Consider again a branch `if a then b else c`\n\n- what is the\nsize of the result? If `b`\n\nand `c`\n\nare arrays, we need to determine whether they\nhave the same size, and if not, generate an existential for the result size. But\nat the point where we are looking at this `if`\n\n, we may not yet know whether they\nare arrays! Many similar cases occurred, and the type system had various\nrestrictions and quirks where the well-typedness of a function would depend in\nsomewhat subtle ways on whether you had put in type annotations. One nice\nproperty however was that if you put in type annotations on all parameters,\nwhich is generally considered good style anyway, then things worked pretty\npredictably. But still, the type checker was not particularly fun to hack on at\nthis point.\n\nEventually, this design - a single program traversal manipulating an\nincreasingly complicated state - stopped being workable. This was when we\nstarted working on [AUTOMAP](2024-06-17-automap.html), a (not yet finished)\nsystem that loosely means that instead of saying `map f x`\n\n, you can just say `f x`\n\nand have the compiler figure out how many `map`\n\ns are needed. During this\nwork, we discovered that AUTOMAP had to work by taking a global view of a\nfunction, typechecking it partially but without inferring specific sizes of\narrays, determining the smallest number of `map`\n\ns needed to make it typecheck\n(by using an ILP solver), and then determine specific sizes. There was no way to\nfit this design into the existing type checker, so I began taking it apart into\nmultiple phases.\n\nThe first phase I extracted was [name\nresolution](https://github.com/diku-dk/futhark/blob/master/src/Language/Futhark/TypeChecker/Names.hs).\nIt does what you would expect: resolves every name in the program, in practice\nby assigning each a unique identifier. This was a small win, but it simplified\nthe type checker a little. The next phase was the conceptually final one:\n[checking for aliases and in-place update\nviolations](https://github.com/diku-dk/futhark/blob/master/src/Language/Futhark/TypeChecker/Consumption.hs).\nThis turned out to be *much* more profitable, as this machinery had always been\nhindered by having to work with incomplete type information. Now it would be\ngiven a *fully well-typed program* and simply had to figure out whether any of\nthe in-place constraints were violated (essentially, whether in-place updates\nare semantically observable).\n\nWe were now left with a remaining core piece: a “term type checker” that did\nboth normal ML-style type inference, but also size inference. As an expediency,\nto make the AUTOMAP research project progress, I did a hacky thing: I wrote an\nentirely new *constraint-based* type checker that did not perform size\ninference, but rather only standard type inference. We call this the *unsized\ntype checker*. Instead of solving type equations on the fly, it generated a set\nof *type constraints* (mostly type equalities), which were solved “offline” by a\nconstraint solver. This “generate-and-solve” approach makes handling complicated\ntype system features more tractable as you don’t have to make decisions based on\npartial information, and is [also used by the Glasgow Haskell\nCompiler](https://www.youtube.com/watch?v=OISat1b2-4k) and the\n[Flix](https://flix.dev/). In our implementation, we used these constraints to\ndrive the AUTOMAP solver, then *threw away those constraints and re-ran the\noriginal size-aware type checker* (along with a bit of extra information\nprovided by AUTOMAP). This *worked*, in that all programs could type check, but\nI think [our paper](https://futhark-lang.org/publications/oopsla24.pdf) puts it\ngenerously when it says “our Automap-enabled type checker is unoptimized”. My\nconcern was not merely the inefficiency of redundant work, but the lack of\nclarity by *duplicated logic* - every type checking rule was duplicated in code!\n\n[The new type checker](#the-new-type-checker)\n\nI still have some hope for AUTOMAP itself, but getting it in a workable state\nrequires an unknown amount of implementation effort. However, during this work I\nhad become attracted to the elegance of constraint-based type checking. The\ncentral advantage is that you can postpone decisions until all information is\navailable. From a human perspective, it is also *much easier* to debug (or\nbenchmark) when you can print out all the constraints and reason about them.\nUnfortunately, our implementation was abominable. Last year I [started work on\ndoing constraint-based type checking without\nAUTOMAP](https://github.com/diku-dk/futhark/pull/2302), and in particular where\nduplication of logic was minimised. The idea was to use the unsized type checker\nto infer “ground” types (meaning types without filling in the specific sizes of\narrays), then do a second pass that performs *only* size inference.\n\nThe main obstacle, and the reason I didn’t finish until now, stemmed from tricky cases related to existential sizes, where existential quantifiers of array sizes must be inserted in just the right place. Consider a function with size-lifted type variables like this:\n\n``` php\ndef apply 'a '^b (f: a -> b) (x: a) =\n  f x\n```\n\nSince `^b`\n\nis size-lifted, it can be instantiated with a type that contains\nexistential quantifiers, in this case meaning that `f`\n\ncan be a function that\nreturns an array of an unknown size.\n\nIf we say `apply iota`\n\n, then the unsized type checker will infer this type for\n`apply`\n\n:\n\n``` php\n(f: i64 -> []i64) -> (x: i64) -> []i64 =\n```\n\nWhen we perform size inference, it is quite important that we infer this type,\nwith existential quantification (`?[n]`\n\n) in return types:\n\n``` php\n(f: i64 -> ?[n].[n]i64) -> (x: i64) -> ?[m].[m]i64\n```\n\nRather than let the return size float all the way to the top as a size parameter:\n\n``` php\n(f: i64 -> [n]i64) -> (x: i64) -> [n]i64\n```\n\nThis incorrect type states that the array returned by `f`\n\nis known before `f`\n\nis\ninvoked at all, and does not depend on its parameter. This is true for *some*\n`f`\n\ns, but not when `f=iota`\n\n.\n\nIn the old type checker, this was done with some clever (oh no…) programming\ndeep in the middle of unification whenever it encountered flexible size-lifted\ntype variables. Since the unsized type checker has now determined all ground\ntypes in advance, no flexible type variables are *ever* encountered, so I had to\nwork out a different solution. The details are too intricate to get into here,\nbut they are ultimately a more explicit handling of the issue. There are still\n*some* ugly parts where I think we never worked out a good theory for size\ninference, but the part of the code that they infect is now a lot smaller than\nit used to be. I still have a vague idea that we should redo all of size\nchecking at one point, perhaps basing it on [bidirectional type\nchecking](https://ncatlab.org/nlab/show/bidirectional+typechecking) instead of\nfull inference, since it really is a dependent type system.\n\nBut that is for the future. With this work, the Futhark type checker now goes through four stages for each function:\n\n- Resolve names.\n- Determine ground types with the unsized type checker.\n- Perform size inference.\n- Check for in-place update violations.\n\n(If we ever finish AUTOMAP, it will go between stages 2 and 3.)\n\nOne lovely property of this design is that if an error is encountered in stage\n*i*, we have full information from stage *i-1* to help explain the error. This\nis a nice change from the original single-pass type checker, where if something\nwent wrong, we would usually have *no* information about those parts of the\nprogram that we have yet to traverse. I suspect a general principle that it’s a\ngood idea to design a type system in “layers”, with each adding more\nrestrictions to the former, such that a program’s meaning can be understood even\nwhen it is not fully type-correct at all layers.\n\nI also found it useful to put as many checks as possible into a separate pass\nthat runs after the main type checking. For example, Futhark has restrictions on\nhigher-order functions: you cannot return them from branches or loops.\nPreviously, we would decorate pertinent type variables with such information,\nwhich complicated the core inference algorithm and sometimes resulted in\nhard-to-understand error messages. Now, we run through the program after type\ninference and look for any such forbidden uses. If any are found, it is easy to\npinpoint the cause: you tell the user that *this* expression is not allowed, and\n*this* is the type we inferred for it.\n\nOne potential downside of this approach is inefficiency - it is usually more expensive to do four things than one thing. Further, some of these stages technically do more than one traversal of the function (in particular size inference) to check for various other problems or update AST decorations.\n\nNow for an aside on compiler efficiency. Futhark was never intended to compile\nquickly. Personally, when I first started working on Futhark, I was inspired by\nsuch projects as [Stalin](https://github.com/barak/stalin) and\n[MLton](http://www.mlton.org/), which were famous for combining unexpected high\nperformance with very long compile times. My aspiration was for people to say of\nFuthark “it sure can generate fast code if you wait for it”. Perhaps I even\nintended to take a bit of pride in compile times being as impractical as Stalin,\nand it would certainly serve as a convenient excuse for why the language wasn’t\nused much. (Although while MLton is slow as compilers go, hardware improvements\nhave made it quite practical these days.) However, since then Futhark has not\nonly been used by (a few) other people; we also use it ourselves to conduct\nresearch into [parallel programming\ntechniques](2026-06-18-data-parallel-pretty-printing.html). It is a lot more fun\nto do this when you can get reasonably prompt feedback from the compiler, and\nhence while the Futhark type checker is certainly by most standards quite slow,\nwe *do* try to some extent to not make it much slower than it currently is.\n\nIn my initial work on splitting the type checker, I observed a 20% slowdown of\n`futhark check`\n\non [a representative\nprogram](https://github.com/diku-dk/futhark-benchmarks/blob/master/finpar/LocVolCalib.fut),\nbut I managed to get that down to about -4% (that is, a *speedup*) through\nvarious optimisations. Most of these were tedious tricks: checking whether some\nwork was truly necessary before undertaking it. Could I have added such\noptimisations to the old type checker as well and thus made it even faster? Yes,\nbut I didn’t. I’m not trying to make the fastest type checker, merely to not\nmake it even slower. (But if someone wants to write fast type checker, please do\nreach out! I think the current design makes it fairly easy to benchmark\ncomponents in isolation.)\n\nAs part of of this work, I also prepared the type checker to allow recursion\n(despite [previous](2016-09-03-language-design.html)\n[concerns](2026-01-20-why-not-tail-recursion.html)), currently gated by the\nsecret environment variable `FUTHARK_ALLOW_RECURSION`\n\n, but that is a story for a\ndifferent day.", "url": "https://wpnews.pro/news/rewriting-the-futhark-type-checker", "canonical_source": "https://futhark-lang.org/blog/2026-07-21-rewriting-the-type-checker.html", "published_at": "2026-07-22 06:36:31+00:00", "updated_at": "2026-07-22 09:53:49.836653+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Futhark", "Hindley-Milner", "Algorithm W"], "alternates": {"html": "https://wpnews.pro/news/rewriting-the-futhark-type-checker", "markdown": "https://wpnews.pro/news/rewriting-the-futhark-type-checker.md", "text": "https://wpnews.pro/news/rewriting-the-futhark-type-checker.txt", "jsonld": "https://wpnews.pro/news/rewriting-the-futhark-type-checker.jsonld"}}