{"slug": "show-hn-sirius-a-type-system-for-array-programming", "title": "Show HN: Sirius – A type system for array programming", "summary": "Developer lorentzj released Sirius, a small imperative, total, polynomially bounded language for pointful array programming, published on GitHub on September 10, 2026. Sirius uses value-dependent types with built-in polynomial PolyDependent ML to statically check array shapes, index bounds, and yield counts, rejecting programs that access a[i+1] in a dot product or add an extra yield in matmul without requiring full refinement types such as Liquid Haskell. The project ships with a live editor embedding a prototype compiler frontend and aims to eliminate runtime shape errors and bounds checks while keeping annotation burden low.", "body_md": "*September 10, 2026*\n\n[Sirius](https://github.com/lorentzj/sirius) is a small, imperative, [total](https://en.wikipedia.org/wiki/Total_functional_programming), polynomially bounded language for pointful array programming.\n\nHaving been stung too many times [massaging PyTorch tensors](https://docs.pytorch.org/docs/stable/generated/torch.nn.Conv3d.html), I set off to create a type system that handles tensor shape bookkeeping. In the process, I explored a new part of PL design space and stumbled on an interesting feature set.\n\nIt turns out we can avoid runtime shape errors and bounds checks -- without the ergonomic strain of full-blown refinement types like [Liquid Haskell](https://ucsd-progsys.github.io/liquidhaskell/) -- provided we sharply restrict shape dynamism. In exchange, we reject more bad programs and expose more complete function contracts. The Sirius type system will also unlock a bit of witchcraft in the backend.\n\nI embedded a live editor with a prototype compiler frontend in this page. Here's a `hello world`: dot product and matrix multiplication.\n\n```\n# typevars, representing natural numbers, come in curly brackets after function name\n# \"a.len == b.len\" is proven at call sites\nfn dot{N}(a: f32[N], b: f32[N]) -> f32:\n    let mut sum = 0.0\n    for i from 0 to N:\n        # i is proven valid index into a and b\n        sum += a[i] * b[i]\n    return sum\n\nfn matmul{I, J, K}(a: f32[I, J], b: f32[J, K]) -> f32[I, K]:\n    for i from 0 to I:\n        for k from 0 to K:\n            # i is proven valid index into a\n            # k valid into b' (b transpose)\n            # dot requires a[i].len == b'[k].len\n            # total yield count is proven I*K\n            # yield populates the return array in row-major order\n            yield dot(a[i], b'[k])\n\nfn test():\n    let arr1 = [\n      [1.0, 2.0, 3.0],\n      [4.0, 5.0, 6.0]\n    ]\n\n    let arr2 = [\n        [7.0, 8.0],\n        [9.0, 1.0],\n        [2.0, 3.0]\n    ]\n\n    # arr1.shape.1 == arr2.shape.0 is proven\n    # annotation is checked\n    let res: f32[2, 2] = matmul(arr1, arr2)\n```\n\nThat program doesn't seem very interesting until you try to change it. The compiler rejects accessing `a[i+1]` in `dot`, adding an extra `yield` in `matmul`, changing the annotation of `res`, or modifying any of the typevar constraints in the function signatures. The typechecker is engineered to break comprehensibly. Diagnostics provide trivial disproofs or counterexamples when possible. Here's some more code:\n\n```\n# polynomial expressions in typevars are valid types\n# \"return.len == A + B\" is proven in function body\nfn concat{A, B}(a: f32[A], b: f32[B]) -> f32[A + B]:\n    yield from a\n    yield from b\n\n# N + 1 for typevar N is also valid type\nfn push{N}(arr: f32[N], item: f32) -> f32[N + 1]:\n    yield from arr\n    yield item\n\nfn fill{N}(val: f32) -> f32[N]:\n    for i from 0 to N:\n        yield val\n\nfn test():\n    let x = [1.0, 2.0, 3.0]\n    \n    # fill(2.0) must be fill{7}(2.0)\n    print concat{3, 7}(x, fill(2.0))\n\n    # concat(...) must be concat{3, 19}(...)\n    print concat(x, fill{19}(2.0))\n\n    # let annotation\n    let zeros: f32[5] = fill(0.0)\n```\n\nSignatures provide shape bounds, accesses are statically checked, and `yield` s are statically counted. In short, we're using value-dependent types with built-in polynomial ([Poly](/api/doc/sirius/solver/poly/struct.Poly.html)[Dependent ML](https://dl.acm.org/doi/10.1145/277650.277732). Since the type system is constrained, the annotation burden stays low.[\\[1\\]](#footnote_1)\n\nWhy make an imperative array language? In my opinion, the humble C-style `for` loop has gotten a bad rap. Some problems are easier to solve in an imperative, looping style, with mutable state and transparent locality. Sirius static analysis keeps most of its power and ameliorates the downsides.\n\nWhat do polynomials have to do with arrays? For one thing, multidimensional array access desugars to a polynomial.\n\n| Array | Access | Desugar | \n|---|---|---|\n\n`A[N]`` A[i]``deref(A + i)`` A[N, M]``A[i, j]`` deref(A + i*M + j)``A[N, M, P]`` A[i, j, k]``deref(A + i*M*P + j*P + k)`` A[N, M, M]``deref(A + i*M^2 + j*M + k)`\nThe desugared expression is a strategy for accessing an element given array coordinates, and it is polynomial in array dimensions. This is a [polynomial integer ring](https://en.wikipedia.org/wiki/Polynomial_ring), $\\mathbb{Z}[x_1,x_2 \\dots x_i]$. Add two `Poly` s together, multiply them, or substitute a variable for a new one, and you'll always get a `Poly` back.\n\n`A[N, M + 1]`` deref(A + i*M + i + j)``A[N, 2*M]`` deref(A + 2*i*M + j)``A[3*i + 2, j + 5]`` deref(A + 3*i*M + 2*M + j + 5)`\nThe Sirius type system permits the programmer to freely express array dimensions and accesses in terms of `Poly` s, as long as the resulting system satisfies the [constraint solver](/api/doc/sirius/solver/index.html).[\\[2\\]](#footnote_2)\n\nNow you might object: why can't the programmer write a function that `yield` s an array of super-polynomial size? This is simple in most languages using a `while` loop or recursion; both are inexpressible in Sirius. Only `for`-iteration between `Poly` bounds is permitted.\n\nDynamic values and shapes cannot mix without blowing up the type system. However, some navigation along their boundary is necessary to write useful programs.\n\nControl flow is dictated by `for`-loops and `if`-statements. Loop iterators are fresh typevars with `FROM <= N < TO` constraints. Facts in `if` conditions are available to the constraint solver inside their scopes. Every `yield` is [counted symbolically](/api/doc/sirius/solver/count/struct.Count.html).\n\n```\n# a challenge for the yield counting engine\n# inner for-body is invoked N(N-1)/2 times\nfn triangle{N}() -> f32[N^2 - N + 1]:\n    for i from 0 to N:\n        for j from 0 to i:\n            yield 0.0\n            yield 1.0\n    yield 2.0\n\n# some non-linear systems can also be solved\nfn nest3d{A, B, C}(arr: f32[A*B*C]) -> f32[A, B, C]:\n    for i from 0 to A:\n        for j from 0 to B:\n            for k from 0 to C:\n                yield arr[i*B*C + j*C + k]\n```\n\nSirius supports [Dex](https://arxiv.org/abs/2104.05372)-style finite index sets over `Poly` s with `Ind`.[\\[3\\]](#footnote_3)` Ind`s may pass through function boundaries and skolemize into the `Poly` algebra via `0 <= Ind(N) < N`.\n\n``` php\nfn find{N}(needle: f32, haystack: f32[N]) -> Ind(N)?:\n    for i from 0 to N:\n        if needle == haystack[i]:\n            # 'Ind(N)' constraint is proven here\n            return i\n    # Ind(N)? is nullable Ind(N)\n    return null\n\nfn test():\n    let mut arr = [1.0, 2.0, 3.0]\n    let k = find(2.0, arr)\n    # flow type sheds ? from k\n    if k != null:\n        # find() constraint is available at call sites\n        # so this access is proven safe\n        arr[k] += 1.0\n        if k > 0:\n            # k has a fresh name\n            # so this is also proven safe \n            arr[k - 1] = 0.0\n```\n\nThe return type of functions like `filter` cannot be expressed using sizes available at the call site, so Sirius also supports existential sizes. The `all`-clause constraints are proven at call sites and given in the function body; the `ex`-clause constraints are proven in the function body and given at call sites.\n\n```\n# read \"for all N, there exists a B such that B <= N\"\nfn filter_greater{all N}{ex B st B <= N}(val: f32, arr: f32[N]) -> f32[B]:\n    # the yield counter finds the bounds [0, N) for this loop\n    for i from 0 to N:\n        if arr[i] > val:\n            yield arr[i]\n\nfn find_all{all N}{ex B st B <= N}(needle: f32, haystack: f32[N]) -> Ind(N)[B]:\n    for i from 0 to N:\n        if needle == haystack[i]:\n            yield i\n\nfn test():\n    let a = [1.0, 2.0, 3.0, 3.0, 4.0]\n    let found = find_all(3.0, a)\n    for i from 0 to found.len:\n        # proven safe since found[i] has type Ind(5)\n        print a[found[i]]\n```\n\nI have argued for imperative array programming, but I plan to build functional and array-oriented facilities over the imperative core, like map and filter, broadcasting, [fancy indexing](https://numpy.org/doc/stable/user/basics.indexing.html#advanced-indexing), and a checked [einops](https://iclr.cc/virtual/2022/oral/6603) primitive integrated like regexes in Perl. The current scalar types are `f32`, `f64`, `i64`, and `bool`; abstracting over numerical types is another motivation for more general parametric polymorphism.\n\nThe `matmul` example glossed over the question of array memory layout. The type system will ultimately track that information, perhaps allowing `@noalias`, `@dense`, and other obligations in signatures. There is no need for `yield` to favor row-major ordering; alternate bijective maps could be supported with other `yield` modes for FFI or layout obligation purposes.\n\nThe short-circuit `?` operator, loop `break` s, and array slices are near-term design problems. Structs are a simple extension of the existing tuple type. [Named tensors](https://nlp.seas.harvard.edu/NamedTensor) then slot in nicely, where `arr.shape` becomes a struct rather than a tuple. Sum types and exhaustive pattern matching are table stakes.\n\nThe `Poly` algebra may be extended with integer division with [quasi-polynomials](https://en.wikipedia.org/wiki/Quasi-polynomial), but only at a steep complexity and runtime cost. The [Alexis King-esque](https://lexi-lambda.github.io/blog/2020/08/13/types-as-axioms-or-playing-god-with-static-types/) construction for strides is `arr[I*J]`, obviating its main use case anyway. Values should be liftable into `Poly`-space using, for instance, `val % N`, allowing implementation of data structures like hashmaps.\n\nSince the counting engine can't express sublinear bounds, Sirius should expose builtins like binary search and $\\mathcal{O}(n \\log n)$ sort. Some true `Poly` systems will always elude the constraint solver (one example today is the perfect square $A^2 - 2AB + B^2 \\geq 0$), so solver development must be guided by systems from practical code for specific applications. Runtime constraint `assert` s can offer an escape hatch.\n\nWhat I've described so far is a toy language, an ergonomics experiment more than anything else. It wouldn't be suitable for use in anger even if it had a fast backend. As I mentioned earlier, this project was borne of my frustration with PyTorch. Zooming out, the GPU is a young technology and it shows in the immaturity of its software ecosystem. The 2026 stack is heavy, fragmented, over-abstracted, and sometimes [not even free](https://developer.nvidia.com/cuda-llvm-compiler).\n\nROCm is closing the performance gap with CUDA as both platforms evolve. AI accelerators are shipping with novel architectures. CPU core counts are rising. As single-core performance progress slows, parallelism increases. Programmers need new tools to harness this new hardware. We are entering the golden age of array languages. Meanwhile, advanced static analysis will become increasingly fashionable as LLM-assisted programming techniques mature.\n\nSirius has no defined scope yet, but its ambitions lean more towards a kernel DSL than a [Mojo](https://mojolang.org/)-scale behemoth. I envision a high-level language that leverages types to drastically streamline optimization on highly parallel hardware. With `Poly` bounds on all memory and compute, the compiler can build tiling, rolling, and fusion strategies -- monomorphizing on shapes and eliding guards. Optimizing compilers [already](https://godbolt.org/z/6xEz3Pan5) try to do this, but they must be conservative in the absence of statically guaranteed constraints. I'll try not to overclaim here as I am not yet initiated in [the dark arts of automatic scheduling](https://arxiv.org/abs/1505.07716).\n\nSirius, or at least its affine fragment, is [polyhedral](https://dl.acm.org/doi/full/10.1145/3674735) by construction, making it uniquely suited to an [MLIR](https://mlir.llvm.org/docs/Dialects/Affine/) toolchain. The compiler can surface everything from memory layout (perhaps using [CuTe](https://arxiv.org/abs/2603.02298)-style hierarchical layout algebra<sup>[\\[4\\]](#footnote_4)</sup>) to runtime complexity, providing detailed performance diagnostics at compile time.\n\nThere are several tools that already accomplish many of Sirius's goals. [Triton](https://triton-lang.org) is an ergonomic Python DSL for GPU kernels with a streamlined backend (a Python DSL may be Sirius's ultimate fate as well). [Futhark](https://futhark-lang.org) is a functional GPGPU language with index functions and an algebra including addition. Sirius will continue to draw inspiration from these projects and academic literature as I hammer out the `1.0` roadmap. Ideas, bugs, and PRs are welcome at the [repo](https://github.com/lorentzj/sirius).\n\n[1] Low-ish. Annotation may be lessened in some cases, à la [lifetime elision](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-elision) in the Rust borrow checker. I will refrain from any kind of global inference because I mostly agree with  [Fernando Borretti on the topic](https://borretti.me/article/type-inference-was-a-mistake). Inference and solving strictly go per-function. [\\[\\]](#backlink_1)\n\n[2] Solving arbitrary `Poly` constraints is impossible, courtesy of [Matiyasevich et al](https://en.wikipedia.org/wiki/Hilbert%27s_tenth_problem). For now, I'm sticking with [Z3](https://github.com/Z3Prover/z3)'s [linear integer arithmetic](https://microsoft.github.io/z3guide/docs/theories/Arithmetic) and a final pass to search for bounded-degree [Handelman](/api/doc/sirius/solver/t2/index.html) representations. Although Z3 could safely check more sophisticated constraints, the checker will inevitably return some false positives. Happily, most constraints are trivial and obtain representationally without invoking Z3 at all. [\\[\\]](#backlink_2)\n\n[3] The Dex paper (p. 11) remarks, “This captures one of the most common uses for the reshape operation... [while] not requiring the type system to solve systems of Diophantine equations to check which reshapes are valid.” Yeah, but what if we do? [\\[\\]](#backlink_3)\n\n[4] Cris Cecka exhibited the *Tao* of Sirius when, asked about compilation times, [he said](https://youtu.be/vzUhbDO_0qk?t=2981) “I make absolutely sure that I never lose track of any static information, ever, because that's the Death of runtime.” The programmer's computer is big so the user's computer can be small. [\\[\\]](#backlink_4)", "url": "https://wpnews.pro/news/show-hn-sirius-a-type-system-for-array-programming", "canonical_source": "https://www.sirius-lang.org/intro", "published_at": "2026-09-10 14:01:35+00:00", "updated_at": "2026-09-10 14:09:33.029447+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Sirius", "lorentzj", "GitHub", "PyTorch", "Liquid Haskell", "PolyDependent ML"], "alternates": {"html": "https://wpnews.pro/news/show-hn-sirius-a-type-system-for-array-programming", "markdown": "https://wpnews.pro/news/show-hn-sirius-a-type-system-for-array-programming.md", "text": "https://wpnews.pro/news/show-hn-sirius-a-type-system-for-array-programming.txt", "jsonld": "https://wpnews.pro/news/show-hn-sirius-a-type-system-for-array-programming.jsonld"}}