Abstract 1 Introduction 2 Overview 3 Staged Language 4 Implicit Arguments and Their Inference 5 Basics of Horsea 6 Further Discussions 7 Implementation Report 8 Related Work 9 Conclusion and Future Work References

Compile-Time Tensor Shape Checking via Staged Shape-Dependent Types

Takashi Suwa ORCID Kyoto University, Japan
Imiron Co., Ltd., Tokyo, Japan
   Atsushi Igarashi ORCID Kyoto University, Japan
Abstract

When writing programs involving matrices or tensors in general, it is desirable to rule out the inconsistency of tensor shapes (i.e., the generalization of matrix sizes) before actual computation. For this purpose, some languages provide dependent types such as 𝙼𝚊𝚝mn, and others offer refinement types to track predicates for shapes. Despite the theoretical maturity, however, such methods are often unhandy for continuous software development due to the requirement of proofs for judging type equality or subtyping; even automated proving is often unsuitable due to its unforeseeable time consumption. To remedy this, our study provides an alternative formalization by using staging. Based on the observation that conditions for the shape consistency can be extracted before running the actual tensor computations in many typical cases, we ensure such consistency by assertions evaluated as compile-time computations, not by proofs. Under this formalization, we can verify the consistency virtually statically in the sense that inconsistencies will be immediately detected as failures during compile-time computation. Our work achieves a mathematical guarantee that successfully generated code is always consistent with respect to tensor shapes. Furthermore, to vastly lessen the burden of adding shape- or stage-related descriptions, we (1) allow shape-related arguments to be implicit and infer them in a best-effort manner, and (2) offer a non-staged surface language that seemingly resembles ordinary dependently-typed languages and translate its programs into the staged core language. By a prototype implementation, we confirm that our language is expressive enough to verify a number of programs, including several examples offered by ocaml-torch.

Keywords and phrases:
Metaprogramming, Staged computation, Dependent types, Refinement types, Tensor shape checking
Copyright and License:
[Uncaptioned image] © Takashi Suwa and Atsushi Igarashi; licensed under Creative Commons License CC-BY 4.0
2012 ACM Subject Classification:
Software and its engineering Functional languages
; Software and its engineering Software verification and validation ; Theory of computation Type structures
Related Version:
Full Version: https://arxiv.org/abs/2604.23807 [63]
Supplementary Material:
Software: https://doi.org/10.5281/zenodo.18501258 [62]
Acknowledgements:
The authors wish to thank the anonymous reviewers and (past) members of our laboratory for various fruitful comments and feedbacks. We are also grateful to Shivankur Gupta for implementing a basic code-generating backend for OCaml during his internship.
Funding:
This work is partially supported by JSPS KAKENHI Grant Numbers 20H00582 and 26H02493, Japan.
Supplementary Material:
Software  (ECOOP 2026 Artifact Evaluation approved artifact): https://doi.org/10.4230/DARTS.12.1.14
Editors:
Robbert Krebbers and Alexandra Silva

1 Introduction

1.1 Background: Tensor Computation and Shape Checking

Nowadays, tensors or multi-dimensional arrays (i.e., vectors, matrices, cuboids, and so on) are nearly everywhere; they have long been used in various methods for mathematical optimization, and recently, due to the growing demand for machine learning, tensors have been intensively used to represent various structures related to deep neural networks (DNN).

When it comes to writing programs involving tensors, it is desirable to rule out the inconsistency of tensor shapes (i.e., the generalization of vector lengths or matrix sizes) before running actual computations. For example, consider the function f that takes three matrices A, B, and C and computes A(B++C++B), where ++ is a binary operator that vertically concatenates two matrices. For verifying consistency, the following constraints must be met: (1) for the use of ++, B and C must have the same number of columns; and (2) for the matrix multiplication, the number of A’s columns must be equal to the number of (B++C++B)’s rows. Checking such constraints during program execution is problematic because it may cause a runtime error due to some trifling shape mismatch bugs only after heavy computations, consuming much time.

A popular approach to addressing this kind of problem is to use dependent type systems. Some languages, such as Idris [6, 7], provide dependent types like 𝚅𝚎𝚌n or 𝙼𝚊𝚝mn, which are the type for vectors of length n and the one for matrices of size m×n, respectively, and assign the matrix multiplication the following type: p,q,r:𝙽𝚊𝚝.𝙼𝚊𝚝pq𝙼𝚊𝚝qr𝙼𝚊𝚝pr. Other methods, such as GraTen [32], offer refinement types like {ν:𝚃𝚎𝚗𝚜𝚘𝚛ν.𝚜𝚑𝚊𝚙𝚎=[m,n]} or {ν:𝚃𝚎𝚗𝚜𝚘𝚛𝚕𝚎𝚗(ν.𝚜𝚑𝚊𝚙𝚎)=3}. As an example of this kind of approach, consider the function f above implemented in an Idris-like hypothetical language:

𝐥𝐞𝐦𝐦𝐚𝐻𝑒𝑖𝑔ℎ𝑡𝑠𝑀𝑎𝑡𝑐ℎkm:m+k+m=k+2m𝐩𝐫𝐨𝐨𝐟𝐞𝐧𝐝
𝐥𝐞𝐭f{j:𝙽𝚊𝚝}{k:𝙽𝚊𝚝}{m:𝙽𝚊𝚝}{n:𝙽𝚊𝚝}
(A:𝙼𝚊𝚝j(k+2m))(B:𝙼𝚊𝚝mn)(C:𝙼𝚊𝚝kn)=
𝐥𝐞𝐭D=𝑣𝑒𝑟𝑡𝐶𝑎𝑡(𝑣𝑒𝑟𝑡𝐶𝑎𝑡BC)B𝐢𝐧𝑚𝑎𝑡𝑀𝑢𝑙𝑡A(𝐫𝐞𝐰𝐫𝐢𝐭𝐞𝐻𝑒𝑖𝑔ℎ𝑡𝑠𝑀𝑎𝑡𝑐ℎkm𝐢𝐧D)

Here, {j:𝙽𝚊𝚝}{n:𝙽𝚊𝚝} are binders for implicit parameters, and 𝑚𝑎𝑡𝑀𝑢𝑙𝑡 and 𝑣𝑒𝑟𝑡𝐶𝑎𝑡 stand for the matrix multiplication and ++, which are assigned types p,q,r:𝙽𝚊𝚝.𝙼𝚊𝚝pq𝙼𝚊𝚝qr𝙼𝚊𝚝pr and p,q,r:𝙽𝚊𝚝.𝙼𝚊𝚝pr𝙼𝚊𝚝qr𝙼𝚊𝚝(p+q)r, respectively. 𝐻𝑒𝑖𝑔ℎ𝑡𝑠𝑀𝑎𝑡𝑐ℎkm is a manually proved lemma used for matching D’s actual type with the one required of D; since D is of type 𝙼𝚊𝚝(m+k+m)n and the function (𝑚𝑎𝑡𝑀𝑢𝑙𝑡A) requires its argument to be of 𝙼𝚊𝚝(k+2m)r for some r, the type-checker must verify that m+k+m=k+2m holds. Working as a proof, the lemma rewrites the former type to the latter through the 𝐫𝐞𝐰𝐫𝐢𝐭𝐞-construct. In general, to type-check an application M1M2, where M1 and M2 are known to have types 𝙼𝚊𝚝M11M12T and 𝙼𝚊𝚝M21M22, respectively, we must prove the type equality 𝙼𝚊𝚝M11M12=𝙼𝚊𝚝M21M22, i.e., that, for i{1,2}, argument expressions M1i and M2i always describe the same value in the given context. Languages of such approaches provide some form of mechanism for proving this equality.

Despite their theoretic maturity and success in many safety-critical fields, however, such verification methods do not seem to be so eagerly applied to relatively typical, continuous software development, especially cases in industry. Although various reasons can be considered for this, we suppose that the following situations would be major factors:

  1. 1.

    The externality of requirements: Requirements imposed on software continuously arise due to social situations and users’ preferences, which are not predictable beforehand.

  2. 2.

    The sequential nature of software development: Checking is performed repeatedly during development. Although we can restrict properties to check to some lightweight ones for frequently performed verification, it would still be better if we do not have to take much time for each run, in order not to harm productivity.

  3. 3.

    Time is literally money: It is costly to let software engineers engaged in development. Moreover, each engineer is available for approximately only 40 hours per week.

Under such circumstances, major issues that hamper adoption of existing methods for tensor shape checking would be some of the following:

  1. A.

    Cumbersomeness of future changes: Methods that require manual proving easily make future changes of programs unwieldy; even slight modification of programs may demand nearly complete amendment of proofs. This does not go along with the condition 1 above, i.e., the unpredictable nature of requirements imposed on software.

  2. B.

    Unpredictable time consumption by automated proving: To reduce the burden of manual proving, some methods provide automated proving by using back-end solvers, possibly with some part of the syntax restricted to a subset suitable for automation. However, such approaches are often too time-consuming or at least take unpredictable time; slight change of properties to check may drastically increase the elapsed time.

  3. C.

    Frequent false-positive errors: As pointed out in some articles like Ascari et al. [3] or a CACM article about the use of static analyses in Facebook (currently known as Meta) [19], methods for verifying detailed properties are likely to cause false positives too easily due to their nature of the overapproximation of program behaviors. Namely, even when the validity is clear for humans, analysis tools often warn, e.g., the existence of a type-level gap and require some form of annotations or proofs. Frequent false-positive errors also add additional cumbersomeness to future changes111“Theoreticians,” including the authors, tend to take false-positive errors for granted (since we cannot achieve sound and complete verification and thereby overapproximations of some kind are necessarily introduced). However, for an affinity with development workflows, it would even be worth considering to strike a balance between soundness properties and the frequency of false positives. .

  4. D.

    Lack of concise support for flexible tensor-handling operations: There are some implicit conversions of tensors frequently utilized in DNN-related programs, such as broadcasting [18, 13]. These conversions are flexible enough to make reasoning tensor shapes non-trivial, at least beyond decidable theories. For example, two tensors of shapes [5, 3, 1, 10] and [3, 4, 10] are addable by broadcasting (specifically, by duplicating the former by 4 along with the third dimension and the latter by 5, respectively). Typical type systems have difficulty in supporting such conversions in a concise manner; they will require proofs for the feasibility of the conversion, and whether the proving is manual or automated, that will also lead to some of the issues A–C above.

In essence, for the adoption to continuous development, it would also be crucial to wipe off the concern about the burden arising from the mismatch between verification methods and development workflows, such as the one due to too many false positives or unpredictable time consumption, not only to establish a method to verify the correctness of programs.

1.2 Basics of Our Language Design

To mitigate the issues A–D above for tensor-manipulating programs, this work provides an alternative formalization of tensor shape checking by using staging (also called staged computation or multi-stage programming [14, 15, 66, 67]). Our key idea is to split tensor computation into two stages: the stage 0, which can be regarded as compile-time, is to verify the shape consistency by assertion checking and generate a specialized program which is proven not to cause any run-time shape mismatch; and the stage 1 is to do actual tensor computation by the specialized program. Our method is based on the observation222Though our formalization may be general enough to be applied to other topics, we have not found other usages that fit this kind of phase separation and thereby focus on tensor shape checking for now. that, in many tensor-manipulating programs, especially the ones that use sophisticated tensor libraries, the computation to check shape consistency can be independent of the actual tensor computations and thus is expected to be lightweight. Let us discuss the core of our idea using a concrete example. The previous example can be expressed as the following program333Notes to those who are not very familiar to multi-stage programs: We use the notations from MetaML [66, 67]. Here, expressions of the forms M (called bracket) and M (called escape) correspond to (hygienic) quasiquotation and splicing in Lisp, respectively. Intuitively, a bracket M evaluates to a code value: for example, 1+4 evaluates to itself, which stands for a piece of code that performs addition of 1 and 4. An escape M is supposed to appear inside a bracket; when M is evaluated to a code value M, the code M is spliced into the surrounding code: For example, 𝐥𝐞𝐭a=𝟷+𝟺𝐢𝐧𝟸a evaluates to 𝟸(𝟷+𝟺). The symbol % (called cross-stage persistence [31, 40, 66, 74] in the literature) signifies that the argument comes from a lower stage. Unlike escapes, the argument can be of any type. :

𝐥𝐞𝐭f=λj:𝙸𝚗𝚝.λk:𝙸𝚗𝚝.λm:𝙸𝚗𝚝.λn:𝙸𝚗𝚝.
λA:𝙼𝚊𝚝%j%(k+𝟸m).λB:𝙼𝚊𝚝%m%n.λC:𝙼𝚊𝚝%k%n.
𝐥𝐞𝐭D=(𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡(m+k)mn)((𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡mkn)BC)B𝐢𝐧
(𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡j(k+𝟸m)n)AD

We use blue and orange to render stage-0 and stage-1 entities, respectively, throughout the paper444Nonetheless, for accessibility reasons, we do not disambiguate stages just by colors. . Numerous shape-related arguments are used in the program for now, but one can see later that many of them can actually be implicit.

Basically, f takes j, k, m, and n as stage-0 parameters, and produces code for (A,B,C)A(B++C++B) specialized for those parameters. Here, 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡 takes three parameters p, q, and r and returns 𝑣𝑒𝑟𝑡𝐶𝑎𝑡p,q,r, where 𝑣𝑒𝑟𝑡𝐶𝑎𝑡p,q,r is the specialized operation that vertically concatenates p×r and q×r matrices. The stage-0 built-in function 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 also takes three parameters p, q, and r and returns 𝑚𝑎𝑡𝑀𝑢𝑙𝑡p,q,r, where 𝑚𝑎𝑡𝑀𝑢𝑙𝑡p,q,r is the matrix multiplication operation specialized for p×q and q×r (if p, q, and r are all non-negative; an assertion failure will be raised otherwise).

To reflect the operational behavior, code-generating functions are assigned types peculiar to staged computation. For example, 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡 is assigned type (p:𝙸𝚗𝚝)(q:𝙸𝚗𝚝)(r:𝙸𝚗𝚝)𝙼𝚊𝚝%p%r𝙼𝚊𝚝%q%r𝙼𝚊𝚝%(p+q)%r by combining dependent function types (x:T1(0))T2(0) and code types T(1), meaning that p, q, and r are available at stage 0 and the resulting code for matrix computation is at stage 1. By the same token, 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 has type (p:𝙸𝚗𝚝)(q:𝙸𝚗𝚝)(r:𝙸𝚗𝚝)𝙼𝚊𝚝%p%q𝙼𝚊𝚝%q%r𝙼𝚊𝚝%p%r.

Although the type of matrices is indexed by size information as before, the type system is not equipped with non-trivial type equality to identify. Instead, the language offers stage-0 casts of the form T1T2 and inserts them through type-checking in order for the casts to be evaluated at compile time to assert the equality of two types. Basically, the above program will be elaborated to the following in a type-guided manner, for example:

𝐥𝐞𝐭f=λj:𝙸𝚗𝚝.λk:𝙸𝚗𝚝.λm:𝙸𝚗𝚝.λn:𝙸𝚗𝚝.
λA:𝙼𝚊𝚝%j%(k+𝟸m).λB:𝙼𝚊𝚝%m%n.λC:𝙼𝚊𝚝%k%n.
𝐥𝐞𝐭D=(𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡(m+k)mn)((𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡mkn)BC)B𝐢𝐧
(𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡j(k+𝟸m)n)A
(𝙼𝚊𝚝%((m+k)+m)%n𝙼𝚊𝚝%(k+𝟸m)%nD)

In this case, to identify 𝙼𝚊𝚝%((m+k)+m)%n and 𝙼𝚊𝚝%(k+𝟸m)%n to ensure the validity of the use of 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡, the cast 𝙼𝚊𝚝%((m+k)+m)%n𝙼𝚊𝚝%(k+𝟸m)%n was inserted by the type-checking procedure, where is an attached label that points to the original application as the source of the failure if the assertion fails. This will check the corresponding arguments in the two types – specifically, ((m+k)+m) and k+𝟸m – are equal using the concrete values of k and m. Thus, if f is applied to concrete integers, say, 𝟺, 𝟷, 𝟸, and 𝟹, the expression f 4 1 2 3 will evaluate to the following code without failure:

λA:𝙼𝚊𝚝%𝟺%𝟻.λB:𝙼𝚊𝚝%𝟸%𝟹.λC:𝙼𝚊𝚝%𝟷%𝟹.
𝐥𝐞𝐭D=𝑣𝑒𝑟𝑡𝐶𝑎𝑡𝟹,𝟸,𝟹(𝑣𝑒𝑟𝑡𝐶𝑎𝑡𝟸,𝟷,𝟹BC)B𝐢𝐧𝑚𝑎𝑡𝑀𝑢𝑙𝑡𝟺,𝟻,𝟹AD

The body of the bracket is basically given type 𝙼𝚊𝚝 4 5𝙼𝚊𝚝 2 3𝙼𝚊𝚝 1 3𝙼𝚊𝚝 4 3 and it is guaranteed not to cause shape mismatch (as far as it is applied to matrices of the designated sizes). Note that no sophisticated machinery is required if one wants to do some type-guided traversal on generated code, possibly for further optimization; a simple type system (with infinitely many base types 𝙼𝚊𝚝 1 1,𝙼𝚊𝚝 1 2,𝙼𝚊𝚝 2 1,) suffices555The types of primitives are also simple. For example, 𝑣𝑒𝑟𝑡𝐶𝑎𝑡𝟸,𝟷,𝟹 is given 𝙼𝚊𝚝 2 3𝙼𝚊𝚝 1 3𝙼𝚊𝚝 3 3. .

In this way, we can verify the consistency during code-generating compile-time computation. We expect that, in many cases, compile-time computation does not take much time and that inconsistencies will be immediately detected as assertion failures. If the stage-0 evaluation succeeds, i.e., produces a code fragment without causing any failure, the resulting code fragment is guaranteed to be consistent with respect to specific tensor shapes. In this sense, we can do tensor shape checking without relying on either manual or automated proving, and thus mitigate the aforementioned issues A, B, and C (We defer to Section 2.2 how the remaining issue D can be resolved in our method). One may see this approach as a compile-time version of manifest contracts [21, 30, 29, 58], hybrid type checking [22, 44], or some similar methods like Lemay et al. [45]. Our approach might also be able to be seen as some kind of staging-based foundation of the template metaprogramming in C++ [36].

Nonetheless, now we have to write many “annotation-like” arguments in exchange for the absence of proofs (like the three arguments in 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡(m+k)mn). Manually adding staging constructs may also be cumbersome. In Section 2, we illustrate that most of these descriptions can actually be cleared away by introducing surface languages.

1.3 Our Contributions

Our contributions can be summarized as follows:

  1. 1.

    Staged core language for compile-time tensor shape checking and the mathematical guarantee of its safety: Based on the formalization of staged computation, we define λ, a two-stage language that enables us to ensure the consistency of tensor shapes at stage-0 (i.e., compile-time) computation. This formalization can be considered handy for real-world use in continuous software development in that it requires neither manual nor automated proofs for type-checking programs. At the same time, our method achieves runtime safety in the sense that, once a code fragment specialized for specific tensor shapes is generated after compile-time computation, it is guaranteed to contain no shape mismatches and thereby can be run safely. Our method also accommodates complex tensor manipulations that are frequently used in DNN-related programs, such as broadcasting [18, 13] or reshaping. Furthermore, our formalization incorporates refinement types [22, 55, 44, 32] so that detailed preconditions expected of stage-0 function parameters can be described by types for the sake of error localization.

  2. 2.

    Extension with implicit arguments and their reconstruction rules: To alleviate the burden of specifying shape-related stage-0 arguments, we define λ{}, an extended version of λ with implicit parameters/arguments, and provide algorithmic rules for the reconstruction of omitted arguments. Although this inference is not complete, it can reconstruct omitted arguments in many typical cases.

  3. 3.

    Horsea, an example non-staged surface language: As an exemplification of adding a non-staged surface language on top of the staged core language, we design Horsea, which frees us from manually adding staging constructs and many shape-related arguments. By using binding-time analysis (BTA) [39, 14, 15], programs in this surface language are translated to λ{} (and finally to λ by reconstructing implicit arguments).

  4. 4.

    Prototype implementation: We implemented a prototype type-checker of Horsea in Haskell based on our method and made it publicly available [61, 62]. This type-checker exemplifies that our method is expressive enough to verify the shape consistency of 10 example programs666Although our method seems able to cover the other remaining examples as well, we could not port them simply due to the lack of time; assigning appropriate types to ocaml-torch’s API and understanding original programs as to unreconstructible implicit arguments requires a bit of time and effort. offered by ocaml-torch [9], an OCaml binding of PyTorch [50], by porting them to Horsea manually and feed them to our type-checker.

We suppose that our method alleviates the issues A–D from the following perspectives:

  1. A.

    Since it only requires type annotations for binders and some implicit arguments that cannot be inferred, it would not hamper future changes to a large extent.

  2. B.

    Since compile-time computations can be described in a usual functional language, one can expect that users can easily estimate the elapsed time for shape checking. Furthermore, for typical cases, the elapsed time will be quite instant (say, less than 0.1 second).

  3. C.

    Our method does not cause false positive errors about tensor shapes in the sense that equations of tensor shapes are tested by concrete values after specializing them.

  4. D.

    As explained in Section 2.2, our method accommodates operations that perform implicit shape conversions such as broadcasting.

We here note that our formalization is not something that completely replaces theorem-proving approaches; our language ensures the shape consistency only after concrete tensor shapes are given for compile-time code generation. By contrast, for verifying the consistency of a tensor-handling library, it is essentially necessary to prove that the library works for any possible combination of tensor shapes, but our method cannot ensure properties that cannot be judged by evaluation, e.g., essentially universally quantified propositions. In this sense, the target population of our language design is end users who are implementing specific heavy tensor computations rather than authors of tensor-handling libraries. Nonetheless, it is possible to utilize our method for testing libraries in a way like property-based testing [12], i.e., by running code generation with many randomly selected combinations of tensor shapes. Also, although in a somewhat awkward manner and with the aid of the so-called 𝐫𝐮𝐧-primitive [66, 69, 31, 42], our method accommodates programs that handle tensors whose sizes are known only at runtime; Section 6.2 discusses how to achieve this.

Figure 1: Overall picture of elaboration.

The rest of the paper is organized as follows: First, Section 2 gives an overview of our method by using running examples, and Section 3 describes the formalization of the staged core language λ and proves its metatheoretic safety properties. Section 4 extends λ with implicit parameters/arguments and gives how to infer omitted arguments. Section 5 provides Horsea as a proof-of-concept, non-staged surface language, and explains the basics of its translation to the staged language by using BTA. After that, Section 6 discusses further extension of our language with some features necessary for real-world use. Finally, Section 7 describes a prototype type-checker implemented based on our method and its example use cases involving ocaml-torch, Section 8 discusses the related work, and Section 9 concludes the paper. Figure 1 depicts the overall procedure of our method explained in Sections 35.

2 Overview

2.1 Implicit Arguments and Their Inference

As we saw in Section 1.2, in exchange for the absence of proofs, programs apparently require many “annotation-like” arguments for tensor shapes. Actually, we can infer many of such arguments even if they are omitted. With implicit arguments, we can write f as follows:

𝐥𝐞𝐭f=λ{j:𝙸𝚗𝚝}.λ{k:𝙸𝚗𝚝}.λ{m:𝙸𝚗𝚝}.λ{n:𝙸𝚗𝚝}.
λA:𝙼𝚊𝚝%j%(k+𝟸m).λB:𝙼𝚊𝚝%m%n.λC:𝙼𝚊𝚝%k%n.
𝐥𝐞𝐭D=𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡(𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡BC)B𝐢𝐧𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡AD

All the shape-related arguments applied to 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡 and 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 are now implicit; they will be inferred by using the type of already defined identifiers (such as built-in functions) and type annotations provided by the user.

For implicit parameters/arguments, we first introduce {x:T1(0)}T2(0), a variant form of stage-0 dependent function types. Functions of this type work exactly the same as those of type (x:T1(0))T2(0) from the operational perspective, but they allow users to omit arguments and require the type-checker to infer an expression that should substitute x for each context. For example, 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡 is now assigned type {p:𝙽𝚊𝚝}{q:𝙽𝚊𝚝}{r:𝙽𝚊𝚝}𝙼𝚊𝚝%p%r𝙼𝚊𝚝%q%r𝙼𝚊𝚝%(p+q)%r, meaning p, q, and r can be implicit. Users can also define a function with implicit arguments by using variant λ-abstractions of the form (λ{x:T(0)}.M(0)). In addition, when one wants to specify arguments explicitly for implicit parameters, applications of the form M1(0){M2(0)} can be used.

The reconstruction of implicit arguments can be done in a type-guided manner. For example, consider the subexpression 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡BC, and let N1(0), N2(0), and N3(0) denote expressions that should be reconstructed for the three parameters. According to the type assigned to 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡, the reconstructed subexpression (𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡{N1(0)}{N2(0)}{N3(0)}) will have type 𝙼𝚊𝚝%N1(0)%N3(0)𝙼𝚊𝚝%N2(0)%N3(0)𝙼𝚊𝚝%(N1(0)+N2(0))%N3(0). Since this subexpression is applied to B, which has type 𝙼𝚊𝚝%m%n, the domain type 𝙼𝚊𝚝%N1(0)%N3(0) must be identical to 𝙼𝚊𝚝%m%n after evaluation. In this case, we can infer that it suffices to just substitute N1(0) and N3(0) with m and n, respectively. Similarly, by comparing 𝙼𝚊𝚝%N2(0)%N3(0) with the type of C (i.e., 𝙼𝚊𝚝%k%n), we can judge that k can be used as N2(0). We have found that this kind of reconstruction can be formalized by using a technique adapted from Xie and Oliveira’s “let arguments go first[73]; other well-trodden approaches such as Hindley–Milner-like unification might also work, but our formalization seems more concise (and still effective enough) in that it suffices to track variables for substitution only locally. Although the reconstruction is essentially incomplete (i.e., even when there exist appropriate expressions, it cannot always infer them), our reconstuction algorithm is fairly effective for typical use cases, as we will report later. Even when it cannot infer an argument, the algorithm can still report the position where the user should explicitly give arguments.

2.2 Support for Implicit Tensor Conversion

As we have mentioned in the issue D, it would be desirable to support some implicit conversions of tensors, such as broadcasting [18, 13], which are frequently used in DNN-related programs. Our language can safely support the tensor addition with broadcasting by providing a code-generating built-in function 𝑔𝑒𝑛𝐴𝑑𝑑 of the following type:

{x:𝙻𝚒𝚜𝚝𝙸𝚗𝚝}{y:𝙻𝚒𝚜𝚝𝙸𝚗𝚝}
𝚃𝚎𝚗𝚜𝚘𝚛%x𝚃𝚎𝚗𝚜𝚘𝚛%y𝚃𝚎𝚗𝚜𝚘𝚛%(𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡xy),

where 𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡:𝙻𝚒𝚜𝚝𝙸𝚗𝚝𝙻𝚒𝚜𝚝𝙸𝚗𝚝𝙻𝚒𝚜𝚝𝙸𝚗𝚝 is a partial function that returns the shape to which two given shapes can be commonly broadcastable. Receiving two lists [𝟻,𝟹,𝟷,𝟷𝟶] and [𝟹,𝟺,𝟷𝟶], for example, 𝑔𝑒𝑛𝐴𝑑𝑑 returns code 𝑎𝑑𝑑[𝟻,𝟹,𝟷,𝟷𝟶],[𝟹,𝟺,𝟷𝟶], where 𝑎𝑑𝑑[𝟻,𝟹,𝟷,𝟷𝟶],[𝟹,𝟺,𝟷𝟶] is the specialized tensor addition of the following type:

𝚃𝚎𝚗𝚜𝚘𝚛%[𝟻,𝟹,𝟷,𝟷𝟶]𝚃𝚎𝚗𝚜𝚘𝚛%[𝟹,𝟺,𝟷𝟶]𝚃𝚎𝚗𝚜𝚘𝚛%[𝟻,𝟹,𝟺,𝟷𝟶].

On the other hand, if 𝑔𝑒𝑛𝐴𝑑𝑑 is applied to two shapes that are not broadcastable to one common shape, it will emit a failure, just as 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 will do for negative integers.

2.3 Error Localization by Stage-0 Refinement Types

One aspect that provides a source for improving the language design is how errors during compile-time computation are reported. Other than casts T1T2, stage-0 functions may also emit a failure since some of them are partial (e.g., 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 cannot take negative integers). Certainly, in both cases, failures can happen only at compile time and can be reported in a somewhat human-friendly manner since they point to a code position by , but the problem is that a reported position does not necessarily identify the direct source of the bug; it can be a position inside a function that does not contain erroneous descriptions, and the reported position should rather be one of the call sites. For example, consider f(𝟷) 1 1 1. This will cause an error with the position in the definition of f at which 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 is applied to j, i.e., the first parameter of f. However, it is the call site of f passing 𝟷, not the definition of f, that this error should be attributed to. It will be better if we have a mechanism to emit a failure when f is applied to a negative value like 𝟷.

To this end, we use refinement types [24, 22, 55, 44] of the form {ν:BN(0)} for stage-0 types. The annotations can be modified from 𝙸𝚗𝚝 to 𝙽𝚊𝚝, which abbreviates {ν:𝙸𝚗𝚝ν𝟶}:

𝐥𝐞𝐭f=λ{j:𝙽𝚊𝚝}.λ{k:𝙽𝚊𝚝}.λ{m:𝙽𝚊𝚝}.λ{n:𝙽𝚊𝚝}.
λA:𝙼𝚊𝚝%j%(k+𝟸m).λB:𝙼𝚊𝚝%m%n.λC:𝙼𝚊𝚝%k%n.

Then, consider an application fM(0). Now that the type for j tracks the precondition required of j, the above application elaborates to f(𝙽𝚊𝚝N(0)) (assuming M(0) elaborates to N(0)), where Trfn(0) is a cast function to assert that the argument satisfies the predicate of the refinement type Trfn(0), and is a label that points to the original application. This will emit a failure if N(0) evaluates to a negative integer and report the position of the call site . Compared to the original situation where failures are raised by 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 used in f, programmers can describe more detailed preconditions expected of arguments, and the type-checker can point to the location where some precondition was violated. We allow refinement types only for stage 0, and thereby the evaluation of such casts happens only at compile time, i.e., runtime evaluation is still free from assertion failures.

We note that refinement types are also beneficial for error localization as to implicit conversion; with refinement types, we can assign 𝑔𝑒𝑛𝐴𝑑𝑑 the following more natural type:

{x:𝙽𝚊𝚝𝙻𝚒𝚜𝚝}{y:{ν:𝙽𝚊𝚝𝙻𝚒𝚜𝚝𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡𝑎𝑏𝑙𝑒xν}}
𝚃𝚎𝚗𝚜𝚘𝚛%x𝚃𝚎𝚗𝚜𝚘𝚛%y𝚃𝚎𝚗𝚜𝚘𝚛%(𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡xy),

where 𝙽𝚊𝚝𝙻𝚒𝚜𝚝 is a basetype for shapes that intuitively works as 𝙻𝚒𝚜𝚝𝙽𝚊𝚝, and 𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡𝑎𝑏𝑙𝑒 judges whether a pair of two shapes is in the domain of 𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡. As a sideline, 𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡:(x:𝙽𝚊𝚝𝙻𝚒𝚜𝚝){ν:𝙽𝚊𝚝𝙻𝚒𝚜𝚝𝑏𝑟𝑜𝑎𝑑𝑐𝑎𝑠𝑡𝑎𝑏𝑙𝑒xν}𝙽𝚊𝚝𝙻𝚒𝚜𝚝 is now a total function.

2.4 Horsea: A Seemingly Dependently-Typed Surface Language

Although many shape-related arguments are now inferred, we still manually add staging constructs, i.e., brackets and escapes . This might be inconvenient for some users, especially those who are unfamiliar with staged computation. As a solution to this cumbersomeness, there are a number of ways to provide a surface language that is less explicit as to staging and to reconstruct where to insert gaps between stages. To provide a proof-of-concept language for the moment, we pick up binding-time analysis (BTA) [39, 14, 15], a well-known classical technique in the literature of partial evaluation. On top of the staged language, we give a surface language named Horsea777In Japanese, seahorses are called tatsu-no-otoshi-go (“dragon’s lost children”) due to their resemblance to Asian-style dragons. We use the name of a seahorse character for our surface language, reflecting the fact that programs in the language are syntactically similar to but internally quite different from those in Idris [6, 7], which was named after a dragon character. that is sheerly non-staged (i.e., does not require manual staging at all). In fact, users can describe f in Horsea as follows:

𝐥𝐞𝐭f{j:𝙽𝚊𝚝}{k:𝙽𝚊𝚝}{m:𝙽𝚊𝚝}{n:𝙽𝚊𝚝}
(A:𝙼𝚊𝚝j(k+𝟸m))(B:𝙼𝚊𝚝mn)(C:𝙼𝚊𝚝kn)=
𝐥𝐞𝐭D=𝑣𝑒𝑟𝑡𝐶𝑎𝑡(𝑣𝑒𝑟𝑡𝐶𝑎𝑡BC)B𝐢𝐧𝑚𝑎𝑡𝑀𝑢𝑙𝑡AD

As one can see, thanks to the omission of some stage-0 arguments and staging constructs, we can finally write a program that is syntactically quite similar to the first code in the Idris-like language. On the contrary, no 𝐫𝐞𝐰𝐫𝐢𝐭𝐞s are necessary for type equality here.

Given a program in Horsea, we perform BTA to find out which parts can be at compile time and insert brackets and escapes accordingly. For this conversion, built-in functions in Horsea (e.g., 𝑣𝑒𝑟𝑡𝐶𝑎𝑡) are associated with those in the staged language (e.g., 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡).

3 Staged Language

In this section, we explain a minimal formalization of our staged core language λ and discuss its metatheoretic properties. Since λ performs elaboration for cast insertion, it has a source syntax and a target syntax. All the proofs can be found in the long version [63].

3.1 Syntax, Typing Rules, and Operational Semantics

The source syntax is defined by the following M(b) and S(b), which range over the set of stage-b expressions and that of stage-b type annotations, respectively (for b{0,1}):

M(0) ::=pcxλx:S(0).M(0)(M(0)M(0))M(1)
M(1) ::=cxλx:S(1).M(1)(M(1)M(1))M(0)
S(0) ::={x:BM(0)}𝚃𝚎𝚗𝚜𝚘𝚛s(x:S(0))S(0)S(1)
S(1) ::=B𝚃𝚎𝚗𝚜𝚘𝚛%M(0)S(1)S(1)
B ::=𝙱𝚘𝚘𝚕𝙸𝚗𝚝𝙵𝚕𝚘𝚊𝚝𝙽𝚊𝚝𝙻𝚒𝚜𝚝

Here, p and c respectively range over the set of built-in functions available only at stage 0 (e.g., 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡) and the set of constants usable at both stages, which includes base constants (e.g., 𝟺𝟸 or 𝚝𝚛𝚞𝚎) and stage-agnostic built-in functions (e.g., + or 𝑚𝑎𝑡𝑀𝑢𝑙𝑡𝟹,𝟺,𝟻). While the application of p may be restricted by refinement predicates (e.g., one cannot pass 𝟷 to 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡), c must be simply-typed. Each constant ranged over by p or c has its own arity, and in particular, base constants are stage-agnostic constants of arity 0. We denote these arities by ar(p) and ar(c). To report the cause of compile-time assertion failures, each occurrence of function application is equipped with a unique label that points to its code position. Users do not have to write these labels; they are simply attached by a pre-processor.

The most essential part of the syntax is that stage-1 tensor types 𝚃𝚎𝚗𝚜𝚘𝚛%M(0) have a stage-0 expression M(0) of type 𝙽𝚊𝚝𝙻𝚒𝚜𝚝 to represent a tensor shape888While 𝙽𝚊𝚝 is just a shorthand for {ν:𝙸𝚗𝚝ν𝟶}, 𝙽𝚊𝚝𝙻𝚒𝚜𝚝 is a base type. This will simplify metatheory. . This gap between tensor types and their argument expressions as to stages ensures, for example, that all the stage-1 binders of the form (λx:𝚃𝚎𝚗𝚜𝚘𝚛%[M1(0),M2(0)].) will be (λx:𝚃𝚎𝚗𝚜𝚘𝚛%[n1,n2].) after compile-time computation and thereby that every tensor in generated code has a specialized shape. Stage-1 types 𝚅𝚎𝚌%M1(0) and 𝙼𝚊𝚝%M1(0)%M2(0) for vectors and matrices can be provided as syntax sugar of 𝚃𝚎𝚗𝚜𝚘𝚛%[M1(0)] and 𝚃𝚎𝚗𝚜𝚘𝚛%[M1(0),M2(0)], respectively. The symbol % in the notation indicates this gap by an analogy to the notion of cross-stage persistence [31, 40, 66, 74]. By contrast, stage-0 tensor types 𝚃𝚎𝚗𝚜𝚘𝚛s have fixed shapes; the metavariable s ranges over the set of finite sequences of natural numbers, and these types can be thought of as a family of countably infinite base types. Stage-0 tensor types occur mostly in stage-0 terms obtained by unlifting generated code and are seldom written by users.

In response to the setting of stage-1 tensor types, stage-0 function types can be dependent ones. This allows, for instance, 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 to be assigned (p:𝙽𝚊𝚝)(q:𝙽𝚊𝚝)(r:𝙽𝚊𝚝)𝙼𝚊𝚝%p%q𝙼𝚊𝚝%q%r𝙼𝚊𝚝%p%r. However, this does not apply to stage 1; by restricting stage-1 function types to non-dependent ones, compile-time assertions are considerably simplified, and many realistic programs can still be supported, as exemplified in Section 7. We also use the notation T1(0)T2(0) for (x:T1(0))T2(0), where xfv(T2(0)).

The target syntax is basically an “assertion-included” variant of the source syntax. It consists of stage-b assertive terms N(b) and assertive types T(b) defined by the following:

N(0) ::=pcxλx:T(0).N(0)N(0)N(0)N(1)
T(1)T(1)L{x:BN(0)}L{x:BN(0)},N(0),cL
N(1) ::=cxλx:T(1).N(1)N(1)N(1)N(0)
T(0) ::={x:BN(0)}𝚃𝚎𝚗𝚜𝚘𝚛s(x:T(0))T(0)T(1)
T(1) ::=B𝚃𝚎𝚗𝚜𝚘𝚛%N(0)T(1)T(1)L::=L.𝐝𝐨𝐦L.𝐜𝐨𝐝

Stage-0 assertions have two forms. The first one is T1(1)T2(1)L, which judges that the stage-1 types T1(1) and T2(1) syntactically coincide after evaluation. Interestingly, adding assertions of this form covers all the necessary cases for checking type equality itself. The attachment L stands for the source of errors when the assertion fails. The other one {ν:BN1(0)}L is used for checking the validity of downcasts. In operational terms, it basically works as an identity function, but before returning the argument (say c) as is, it checks whether the argument satisfies the predicate N1(0), i.e., whether [c/ν]N1(0) evaluates to 𝚝𝚛𝚞𝚎. The variant {ν:BN1(0)},N2(0),cL called an active check is the intermediate form of this assertion process, where N2(0) is a “refinement proposition” (i.e., the application of the refinement predicate N1(0) to the tested value c) under evaluation. It also keeps c separately so that it will evaluate to c when the assertion passes. Only the first one of these two assertion forms is new; the latter device is ported from the context of manifest contracts [30, 58].

ΓbM(b):T(b)N(b)

Γ1M(1):T(1)N(1)Γ0M(1):T(1)N(1)S0-Brkt0(p)=T(0)Γ0p:T(0)pS0-Cst0

%(c)=τ(1)Γ0c:(τ(1))cS0-CstPΓ0S1(0)T1(0)Γ,x:(T1(0))00M2(0):T2(0)N2(0)Γ0(λx:S1(0).M2(0)):(x:T1(0))T2(0)(λx:T1(0).N2(0))S0-Abs

Γ0M1(0):(x:T11(0))T12(0)N1(0)Γ0M2(0):T2(0)N2(0)ΓT2(0)T11(0)N0(0)Γ0(M1(0)M2(0)):[N0(0)N2(0)/x]T12(0)N1(0)(N0(0)N2(0))S0-App   Γ(x)=(T(0))0Γ0x:T(0)xS0-Var

Γ0M(0):T(1)N(0)Γ1M(0):T(1)N(0)S1-Esc   %(c)=τ(1)Γ1c:τ(1)cS1-CstP   Γ(x)=(T(1))1Γ1x:T(1)xS1-Var

Γ1S1(1)T1(1)Γ,x:(T1(1))11M2(1):T2(1)N2(1)xfv(T2(1))Γ1(λx:S1(1).M2(1)):T1(1)T2(1)(λx:T1(1).N2(1))S1-Abs

Γ1M1(1):T11(1)T12(1)N1(1)Γ1M2(1):T2(1)N2(1)T2(1)||1T11(1)Γ1(M1(1)M2(1)):T12(1)N1(1)(T2(1)T11(1)N2(1))S1-App

ΓbS(b)T(b)

Γ,ν:({ν1:B𝚝𝚛𝚞𝚎})00M(0):{ν2:𝙱𝚘𝚘𝚕N(0)}N(0)Γ0{ν:BM(0)}{ν:BN(0)}ST0-Base

Γ0𝚃𝚎𝚗𝚜𝚘𝚛s𝚃𝚎𝚗𝚜𝚘𝚛sST0-Tensor   Γ1BBST1-Base

Γ0S1(0)T1(0)Γ,x:(T1(0))00S2(0)T2(0)Γ0(x:S1(0))S2(0)(x:T1(0))T2(0)ST0-Arr   Γ1S(1)T(1)Γ0S(1)T(1)ST0-Code

Γ1S1(1)T1(1)(for i{1,2})Γ1S1(1)S2(1)T1(1)T2(1)ST1-ArrΓ0M(0):{ν:𝙽𝚊𝚝𝙻𝚒𝚜𝚝N(0)}N(0)Γ1𝚃𝚎𝚗𝚜𝚘𝚛%M(0)𝚃𝚎𝚗𝚜𝚘𝚛%N(0)ST1-Tensor

Figure 2: Source typing rules with assertion insertion.

Typing judgments are defined as ΓbM(b):T(b)N(b) (for b{0,1}), which can be read as “under the type environment Γ, the source expression M(b) has type T(b) and elaborates to N(b) by assertion insertion.” The structure of type environments are defined by: Γ::=Γ,x:(T(0))0Γ,x:(T(1))1, i.e, Γ tracks the stage at which each variable was bound. Figure 2 shows the typing rules for these judgments. The rules S0-Brkt and S1-Esc are peculiar to staged computation and are natural extensions from the literature. S0-Abs and S1-Abs elaborate the type annotations by using the judgments ΓbS(b)T(b). Also, for constants, we have S0-Cst0, S0-CstP, and S1-CstP. Here, we use two environments 0 and %; the former maps stage-0-specific built-in functions p to stage-0 types T(0), and the latter works similarly for stage-agnostic constants c. The metavariable τ(1) and its unlifting (τ(1))=T(0) will be introduced shortly. Entries are like the following:

0(𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡)=(p:𝙽𝚊𝚝)(q:𝙽𝚊𝚝)(r:𝙽𝚊𝚝)𝙼𝚊𝚝%p%q𝙼𝚊𝚝%q%r𝙼𝚊𝚝%p%r,
%(𝑚𝑎𝑡𝑀𝑢𝑙𝑡𝟹,𝟺,𝟻)=𝙼𝚊𝚝%𝟹%𝟺𝙼𝚊𝚝%𝟺%𝟻𝙼𝚊𝚝%𝟹%𝟻,%(𝟺𝟸)=𝙸𝚗𝚝.

T1(1)||1T2(1) ΓLT1(0)T2(0)N(0)

ΓL𝚃𝚎𝚗𝚜𝚘𝚛s𝚃𝚎𝚗𝚜𝚘𝚛sλx:𝚃𝚎𝚗𝚜𝚘𝚛s.xI-Tensor
𝚃𝚎𝚗𝚜𝚘𝚛%N1(0)||1𝚃𝚎𝚗𝚜𝚘𝚛%N2(0)  ​ΓL{ν:BN1(0)}{ν:BN2(0)}{ν:BN2(0)}LI-Rfn
B||1B   T11(1)||1T21(1)T12(1)||1T22(1)T11(1)T12(1)||1T21(1)T22(1)   T1(1)||1T2(1)ΓLT1(1)T2(1)T1(1)T2(1)LI-Code
ΓL.𝐝𝐨𝐦T21(0)T11(0)N1(0)fdom(Γ){x}xdom(Γ){x,f}Γ,x:(T11(0))0L.𝐜𝐨𝐝[x/x]T12(0)T22(0)N2(0)ΓL(x:T11(0))T12(0)(x:T21(0))T22(0)λf:(x:T11(0))T12(0).λx:T21(0).𝐥𝐞𝐭x:T11(0)=N1(0)x𝐢𝐧N2(0)(fx)I-Arr

Figure 3: The rules for type compatibility and cast term generation.

The most distinctive typing rules are those for applications, i.e., S0-App and S1-App. Here, T1(1)||1T2(1) judges type compatibility, i.e., type equivalence ignoring the difference of argument expressions, and ΓLT1(0)T2(0)N0(0) generates assertive cast terms N0(0). Figure 3 displays the rules for these judgments. Among these rules, I-Code is the core of the cast term generation; it checks the compatibility of given two types and simply produces an “atomic” cast term T1(1)T2(1)L. I-Arr, which works for functions, is another characteristic rule. It produces two cast terms for domain types and codomain types, respectively, and combines them. Thanks to this rule, we can handle higher-order programs without any hindrance. This mechanism is partially inspired by GraTen [32] and FHσ [58].

N(0)0(N(0)L)

N1(0)0N1(0)N1(0)N2(0)0N1(0)N2(0)E0-App1   N1(0)0LN1(0)N2(0)0LE0-App1F

T1(1)1T1(1)T1(1)T2(1)L0T1(1)T2(1)LE0-Ass1   τ1(1)τ2(1)τ1(1)τ2(1)L0LE0-AssFail

τ(1)τ(1)L0λx:τ(1).xE0-AssPass   (λx:T11(0).N12(0))v2(0)0[v2(0)/x]N12(0)E0-Beta

δ(a1,c2)=qa1c20qE0-Delta   {ν:BN1(0)}Lc20{ν:BN1(0)},[c2/ν]N1(0),c2LE0-RfnStart

N(1)1N(1)N(1)0N(1)E0-BrktN(0)0N(0){ν:BN1(0)},N(0),c2L0{ν:BN1(0)},N(0),c2LE0-RfnAct

{ν:BN1(0)},𝚝𝚛𝚞𝚎,c2L0c2E0-RfnPass   {ν:BN1(0)},𝚏𝚊𝚕𝚜𝚎,c2L0LE0-RfnFail

N(1)1(N(1)L)

N1(1)1N1(1)N1(1)N2(1)1N1(1)N2(1)E1-App1   N1(1)1LN1(1)N2(1)1LE1-App1F

T(1)1T(1)λx:T(1).N(1)1λx:T(1).N(1)E1-Abs1   N(1)1N(1)λx:τ(1).N(1)1λx:τ(1).N(1)E1-Abs2

N(0)0N(0)N(0)1N(0)E1-Esc   N(0)0LN(0)1LE1-EscF   v(1)1v(1)E1-Cancel

T(1)1(T(1)L)

N(0)0L𝚃𝚎𝚗𝚜𝚘𝚛%N(0)1LET1-TensorFT1(1)1L(T1(1)T2(1))1LET1-Arr1F

N(0)0N(0)𝚃𝚎𝚗𝚜𝚘𝚛%N(0)1𝚃𝚎𝚗𝚜𝚘𝚛%N(0)ET1-Tensor   T1(1)1T1(1)(T1(1)T2(1))1(T1(1)T2(1))ET1-Arr1

Figure 4: Reduction relations (selective; see Figures 11 and 12 in [63] for full definition).

Assertive terms are equipped with staged call-by-value small-step reduction relations N(b)b(N(b)L) defined in a straightforward manner except that (1) argument expressions in assertions are evaluated, and for this purpose, type expressions in the programs are evaluated as well as terms; (2) when an assertion for type equality passes, it evaluates to an identity function; and that (3) when assertion fails, it evaluates to a special symbol L standing for failures, and then the result L is propagated to the whole program. Figure 4 displays the rules for this operational semantics, where stage-b values v(b) (for b{0,1}) and stage-1 type values τ(1) are defined by the following:

v(0)::=aλx:T(0).N(0){x:BN(0)}Lv(1)
v(1)::=cxλx:τ(1).v(1)v(1)v(1)τ(1)::=B𝚃𝚎𝚗𝚜𝚘𝚛%sτ(1)τ(1)

Intuitively, v(1) and τ(1) correspond to “completed” code fragments and type annotations, respectively. Here, a ranges over the set of runtime constants, which consists of base constants and possibly partially applied built-in functions (e.g., 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 or 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡 3). More formally, a ranges over the following: (i) c with ar(c)=0 (i.e., base constants), (ii) (pc1ck) such that 0k<ar(p), and (iii) (cc1ck) such that 0k<ar(c). Note that, unlike stage-1 type expressions, we do not have to evaluate stage-0 type expressions; while stage-1 types should remain in produced code (possibly for further post-process optimization), stage-0 types are all thrown away by β-reduction through stage-0 computation.

To deal with applications of built-in functions, E0-Delta uses the so-called delta reduction δ, which maps a pair consisting of an operation and a complete array of operands to a value of the form q::=cc. Entries of δ are like the following:

δ(+,(𝟺𝟸,𝟻𝟽))=𝟿𝟿,δ(𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡,(𝟹,𝟺,𝟻))=𝑚𝑎𝑡𝑀𝑢𝑙𝑡𝟹,𝟺,𝟻.

As an abuse of notations, we often write δ(ac) for δ(c^,(c1,,ck,c)) (resp. δ(p,(c1,,ck,c))) when a=(c^c1ck) (resp. a=(pc1ck)).

When code generation successfully terminates with a code value v(1), such v(1) can be regarded as a stage-0 term by unlifting operations (v(1))=N(0) and (τ(1))=T(0):

(x):=x(λx:τ(1).v(1)):=λx:(τ(1)).(v(1))(v1(1)v2(1)):=(v1(1))(v2(1))
(c):=c(𝚃𝚎𝚗𝚜𝚘𝚛%s):=𝚃𝚎𝚗𝚜𝚘𝚛s(B):={ν:B𝚝𝚛𝚞𝚎}
(τ1(1)τ2(1)):=(x:(τ1(1)))(τ2(1))(where xfv((τ2(1))))

One can then evaluate N(0):=(v(1)) as ordinary runtime execution, which will not cause any failure since N(0) contains no assertions.

Lastly, we note that, while reduction rules make sense to open terms as well, we suppose that only closed terms can step, i.e., all the reduction rules implicitly require that the reduced term be closed. Here, by closed terms, we mean those in which no stage-0 variables freely occur; stage-1 variables are not considered, and hence x+𝟷 is a closed term, for example. This restriction is crucial for our metatheory, specifically for proving cotermination [58].

3.2 Metatheory

ΓbN(b):T(b)

Γ0{ν:BN(0)}%(c)=B[c/ν]N(0)0𝚝𝚛𝚞𝚎Γ0c:{ν:BN(0)}T0-RfnPred
ΓΓ(x)=(T(0))0Γ0x:T(0)T0-Var   Γ0N1(0):(x:T11(0))T12(0)Γ0N2(0):T11(0)Γ0N1(0)N2(0):[N2(0)/x]T12(0)T0-App
Γ1T1(1)Γ1T2(1)T1(1)||1T2(1)xdomΓΓ0T1(1)T2(1)L:(x:T1(1))T2(1)T0-Ass   Γ%(c)=τ(1)Γ0c:(τ(1))T0-CstP
Γ0N(0):T(0)T(0)0T(0)Γ0T(0)Γ0N(0):T(0)T0-TyEquiv   Γ0(p)=T(0)Γ0p:T(0)T0-Cst0
Γ0T1(0)Γ,x:(T1(0))00N2(0):T2(0)Γ0(λx:T1(0).N2(0)):(x:T1(0))T2(0)T0-Abs   Γ1N(1):T(1)Γ0N(1):T(1)T0-Brkt
Γ0{ν:BN(0)}Γ0{ν:BN(0)}xdomΓΓ0{ν:BN(0)}L:(x:{ν:BN(0)}){ν:BN(0)}T0-RfnΓ0N(0):T(1)Γ1N(0):T(1)T1-Esc
Γ0{ν:BN1(0)}Γ0N2(0):{ν0:𝙱𝚘𝚘𝚕𝚝𝚛𝚞𝚎}%(c)=B[c/ν]N1(0)0N2(0)Γ0{ν:BN1(0)},N2(0),cL:{ν:BN1(0)}T0-RfnActΓ1N1(1):T11(1)T12(1)Γ1N2(1):T11(1)Γ1N1(1)N2(1):T12(1)T1-App
Γ1T1(1)Γ,x:(T1(1))11N2(1):T2(1)xfv(T2(1))Γ1(λx:T1(1).N2(1)):T1(1)T2(1)T1-AbsΓ%(c)=τ(1)Γ1c:τ(1)T1-CstP
Γ1N(1):T(1)T(1)1T(1)Γ1T(1)Γ1N(1):T(1)T1-TyEquivΓΓ(x)=(T(1))1Γ1x:T(1)T1-Var

Figure 5: Target typing for metatheory.

For the purpose of proving type safety, assertive terms are also assigned types by declarative target typing of the form ΓbN(b):T(b). Figure 5 displays the rules for these judgments. Some of the rules depend on type equivalences T1(b)bT2(b) and well-formedness judgments ΓbT(b) and Γ defined in Figures 13 and 14 in the long version [63]. The equivalences b are necessarily introduced to prove Preservation as to function applications.

We first prove that well-typed source terms are always elaborated to assertive terms well-typed under target typing, by relatively straightforward induction:

Theorem 1 (Soundness of Assertion Insertion).

If Γ and ΓbM(b):T(b)N(b), then ΓbN(b):T(b).

Proving Preservation and Progress [52] is much more challenging due to the combination of the δ-reduction, the type equivalences b, and the dependent nature of our typing. To prove Preservation as to built-in functions, we must assume some natural properties on 0, %, and δ. For example, since δ(𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡,(𝟹,𝟺,𝟻))=𝑣𝑒𝑟𝑡𝐶𝑎𝑡𝟹,𝟺,𝟻 holds, 𝑔𝑒𝑛𝑉𝑒𝑟𝑡𝐶𝑎𝑡 3 4 5 and 𝑣𝑒𝑟𝑡𝐶𝑎𝑡𝟹,𝟺,𝟻 must have equivalent types. However, we cannot use the type equivalences to describe such assumptions; because the definition of b will depend on δ, we have to avoid the dependency of the reverse direction in order not to make the validity circular. To this end, we have to use reduction relations, rather than equivalences. We put the resulting descriptions as Assumption 8 in the long version [63] because they are one-page long.

Another challenging point arising after identifying the above assumptions is how to define the type equivalences b precisely. They must be at least compatible with the β-equivalence, but the β-equivalence itself is actually too loose; in order to prove Preservation as to δ, we must define b carefully so that they preserve the “reducibility” of refinement predicates to 𝚝𝚛𝚞𝚎. For this purpose, following Sekiyama et al. [58], we can extend and use the common subexpression reduction (CSR) equivalence [29, 58], which is the equivalence spanned by the relation that allows the usual call-by-value reduction for arbitrary closed subexpressions.

Under these settings, we have proved the following safety properties, where 1Γ means that all the entries in Γ are of the form (x:(T(1))1):

Theorem 2 (Preservation).

If ΓbN(b):T(b) and N(b)bN(b), then ΓbN(b):T(b).

Theorem 3 (Progress).

If 1Γ and ΓbN(b):T(b), then we have one of the following: (1) N(b)bL; (2) there exists N(b) such that N(b)bN(b); or (3) N(b) is a value.

γv(1):τ(1)

γ(x)=τ(1)γx:τ(1)G-Var   γ,x:τ1(1)v2(1):τ2(1)γ(λx:τ1(1).v2(1)):τ1(1)τ2(1)G-Abs
%(c)=τ(1)γc:τ(1)G-Cst   γv1(1):τ2(1)τ(1)γv2(1):τ2(1)γv1(1)v2(1):τ(1)G-App

Figure 6: The typing rules for generated code.

For proving the safety of generated code, we use an additional type judgment γv(1):τ(1) defined by the rules in Figure 6, where γ is defined by: γ::=γ,x:τ(1). Unlike the target typing, this type system uses only value types τ(1) and does not depend on the type equivalences; as mentioned in Section 1.2, this is essentially a simply-typed setting with countably infinite number of base types (𝙼𝚊𝚝 1 1, 𝙼𝚊𝚝 1 2, 𝙼𝚊𝚝 2 1, and so on). The following states that generated code is always well-typed under the -rules, where Γγ is the evident pointwise equivalence based on 1:

Lemma 4.

If 1Γ, Γ1v(1):T(1), and Γγ, then there exists τ(1) such that γv(1):τ(1) and T(1)1τ(1).

However, there is one remaining challenge as to proving this lemma; the following property is crucial (since the -rules are independent of b):

Lemma 5.

τ1(1)1τ2(1) implies τ1(1)=τ2(1).

We prove this by separating it into two: (1) τ1(1)1τ2(1) implies τ1(1)1τ2(1), where b is the standard β-equivalence; and (2) τ1(1)1τ2(1) implies τ1(1)=τ2(1). While the former is straightforward, the latter requires slight attention: the reduction includes the rule τ(1)τ(1)L0(λx:τ(1).x), which is not left-linear in the sense that the metavariable τ(1) appears more than once on the left-hand side. In such a reduction system, confluence is often hard to show or even broken. Nonetheless, in our cases, it turns out to be sufficient to use a standard syntactic approach to consistency, which defines parallel reduction corresponding to the equivalence and prove the uniqueness of normal forms via confluence. Thanks to Lemma 5, we can finally prove Lemma 4.

The following lemma can easily be shown, where (γ)=Γ is the evident unlifting:

Lemma 6.

γv(1):τ(1) implies (γ)0(v(1)):(τ(1)).

Since unlifted code does not contain p or assertions and is typeable without the type equivalences, the combination of the theorems and lemmata above ensures that, if the source program elaborates to a term and the term evaluates to a code value without failures, we will not have runtime shape mismatches when running the produced code:

Corollary 7.

If 0M(0):T(1)N(0) and N(0)0v0(0), then v0(0) is of the form v(1), and this v(1) satisfies one of the following: (1) there exists v1(0) such that (v(1))0v1(0), or (2) the evaluation of (v(1)) does not halt999More precisely, unless extended with recursive functions, λ is strongly normalizing, and thus (2) does not happen. .

4 Implicit Arguments and Their Inference

To alleviate the burden of manually adding shape-related stage-0 arguments in λ, we offer λ{}, an extension of λ with implicit parameters/arguments, as mentioned in Section 2.1. This section formalizes λ{} and explains how to infer implicit arguments.

First, the source syntax is extended with a variant of λ-abstractions and applications101010Some contexts will newly allow λ-abstractions without a type annotation (i.e., λx.(0)). This is closely related to how the reconstruction algorithm is designed; B0-AbsNoAnnot and B1-AbsNoAnnot displayed in Figures 15 and 16 in the long version [63] deal with such λ-abstractions. :

(0) ::=pcxλx:𝒮(0).(0)λx.(0)((0)(0))
λ{x:𝒮(0)}.(0)((0){(0)})(0)_(1)
(1) ::=cxλx:𝒮(1).(1)λx.(1)((1)(1))(0)
𝒮(0) ::=B𝚃𝚎𝚗𝚜𝚘𝚛s(x:𝒮(0))𝒮(0){x:𝒮(0)}𝒮(0)𝒮(1)
𝒮(1) ::=B(𝚃𝚎𝚗𝚜𝚘𝚛%(0))𝒮(1)𝒮(1)

The construct (λ{x:𝒮(0)}.(0)) works as λ-abstractions whose parameter can be implicit, and ((0){(0)}) can be used to specify an argument for such parameters explicitly. The form (0)_ lies between complete omission and explicit designation; it indicates the existence of (0)’s implicit parameter, but does not specify a concrete expression for it. This is useful, e.g., for specifying an argument for only the second implicit parameter by ((0)_{2(0)}).

As mentioned earlier in Section 2.1, we reconstruct implicit arguments through type-checking as well as inserting compile-time assertions. This process can be formalized by utilizing “let arguments go first[73], which is a variant of bidirectional type-checking [53, 20]; when checking applications, we first traverse the argument to obtain its type and then inspect the function, not the other way around as usual. To handle dependent function types and cast insertion, our formalization extends Xie and Oliveira’s original work in several aspects, such as the form of application contexts and return types, which will be explained below.

𝒢Ψ0(0)R(0)N(0)

𝒢(x)=(𝒯(0))0𝒢Ψ0𝒯(0)<:R(0)𝒢Ψ0xR(0)xB0-Var

𝒢02(0)𝒯2(0)N2(0)𝒢Ψ,(N2(0):𝒯2(0))001(0)(N0(0)/𝒯11(0))0R12(0)N1(0)𝒢Ψ0(1(0)2(0))R12(0)N1(0)(N0(0)N2(0))B0-App

𝒢0𝒮1(0)𝒯1(0)𝒢,x:(𝒯1(0))002(0)𝒯2(0)N2(0)𝒢0λx:𝒮1(0).2(0)(x:𝒯1(0))𝒯2(0)λx:𝒯1(0).N2(0)B0-AbsAnnot1

𝒢Ψ1(1)(Di)i=1mT(1)N(1)𝒢Ψ0(1)(Di)i=1mT(1)N(1)B0-Brkt

𝒢02(0)𝒯2(0)N2(0)𝒢Ψ,(N2(0):𝒯2(0))001(0){N0(0)/𝒯11(0)}R12(0)N2(0)𝒢Ψ0(1(0){2(0)})R12(0)N1(0)(N0(0)N2(0))B0-AppImp

𝒢Ψ,_01(0)𝐟𝐢𝐥𝐥{N2(0):𝒯2(0)}R(0)N1(0)𝒢Ψ01(0)_R(0)N1(0)N2(0)B0-FillImp

𝒢Ψ01(0)𝐢𝐧𝐬𝐞𝐫𝐭{N2(0):𝒯2(0)}R(0)N1(0)𝒢Ψ01(0)R(0)N1(0)N2(0)B0-InsertImp

𝒢Ψ1(1)R(1)N(1)

𝒢Ψ0(0)(Di)i=1mT(1)N(0)𝒢Ψ1(0)(Di)i=1mT(1)N(0)B1-Esc

𝒢(x)=(T(1))1𝒢Ψ1T(1)<:R(1)𝒢Ψ1xR(1)xB1-Var   0(c)=τ(1)𝒢Ψ1τ(1)<:R(1)𝒢Ψ1cR(1)cB1-CstP

𝒢12(1)T2(1)N2(1)𝒢Ψ,(T2(1))111(1)(N0(0)/T11(1))1R12(1)N1(1)𝒢Ψ1(1(1)2(1))R12(1)N1(1)(N0(0)N2(1))B1-App

𝒢0𝒮(0)𝒯(0) 𝒢1𝒮(1)T(1)

𝒢0(0)𝒯(0)N(0)𝒢𝒯(0){ν:𝙽𝚊𝚝𝙻𝚒𝚜𝚝𝚝𝚛𝚞𝚎}N0(0)𝒢1(𝚃𝚎𝚗𝚜𝚘𝚛%(0))𝚃𝚎𝚗𝚜𝚘𝚛%(N0(0)N(0))BT1-Tensor

𝒢1𝒮(1)T(1)𝒢0𝒮(1)T(1)BT0-Code   𝒢0𝒮1(0)𝒯1(0)𝒢,x:(𝒯1(0))00𝒮2(0)𝒯2(0)𝒢0{x:𝒮1(0)}𝒮2(0){x:𝒯1(0)}𝒯2(0)BT0-Imp

Figure 7: Rules for inferring implicit arguments (selective; see Figures 15 and 16 in [63] for full definition).

Figure 7 displays the rules for the new elaboration that simultaneously performs type-checking, assertion insertion, and the reconstruction of implicit arguments. The main judgments for stage-b expressions are 𝒢Ψb(b)R(b)N(b) (for b{0,1}), which can be understood as “under the type environment 𝒢 and the application context Ψ, the expression (b) is well-typed and can be elaborated to the target term N(b), and its type is instantiated to R(b) at this context.” A number of new devices are introduced for this elaboration, so we will explain them one by one. First, as can be seen from the judgments, the syntax of target terms remains the same as λ; we keep using N(b). Type environments 𝒢 used here are normal entities that just associate stage-b variables with stage-b types for b{0,1}, respectively: 𝒢::=𝒢,x:(𝒯(0))0𝒢,x:(T(1))1. The sole notable thing is that, while we continue using T(1) for stage-1 types, the syntax of stage-0 types is extended with those for functions with implicit parameters as follows:

𝒯(0) ::=B𝚃𝚎𝚗𝚜𝚘𝚛s(x:𝒯(0))𝒯(0){x:𝒯(0)}𝒯(0)T(1)

Some rules in Figure 7 uses the evident injection 𝒯(0) of 𝒯(0)-types to T(0)-types, which simply forgets the difference between (x:𝒯1(0))𝒯2(0) and {x:𝒯1(0)}𝒯2(0).

One of the essential devices to do “let arguments go first” is application contexts Ψ. When checking an expression (0) that is expected to be a function, the stack Ψ provides information about the outside, i.e., what kind of sequence is passed as arguments of (0):

Ψ ::=Ψ,(N(0):𝒯(0))0Ψ,{N(0):𝒯(0)}Ψ,_Ψ,(T(1))1

An entry (N(0):𝒯(0))0 (resp. {N(0):𝒯(0)}0) stands for the existence of a stage-0 mandatory argument (resp. an explicitly specified argument for an implicit parameter) of type 𝒯(0) whose elaboration result is N(0). Unlike the original work [73], we push actual arguments to Ψ as well as the types of the arguments to instantiate the type of the function applied to them, as explained later. The other two forms can be understood in the same way; _ and (T(1))1 represents the existence of _ and a stage-1 argument of type T(1), respectively. The rules B0-App, B0-AppImp, B0-FillImp, and B1-App appropriately push these entries to the stack Ψ to check the function after traversing the argument first. The stacked entries will be popped by some rules, such as B0-Var or B1-Var. B0-Var instantiates the type of the variable guided by Ψ by using an auxiliary judgment 𝒢Ψ0𝒯(0)<:R(0). This instantiates 𝒯(0) to an extended return type R(0) so that the domain types can match the types of the arguments passed to the variable. B1-Var and 𝒢Ψ1T(1)<:R(1) do basically the same thing for stage-1 variables. The syntax of R(0) and R(1) is defined by the following:

R(0) ::=DR(0)𝒯(0)
R(1) ::=DR(1)T(1)
D ::=(N(0)/𝒯(0))0{N(0)/𝒯(0)}(N(0)/T(1))1
𝐟𝐢𝐥𝐥{N(0):𝒯(0)}𝐢𝐧𝐬𝐞𝐫𝐭{N(0):𝒯(0)}

As the name suggests, extended return types basically work as types; DR(0) can be seen as a function type just by regarding the extended domain D as 𝒯(0) or T(1). The difference from usual types is that extended domains are equipped with a cast term or a reconstructed term that works as a “feedback” for the corresponding application site. B0-App inserts into the elaborated application the cast term N0(0) returned by the traversal of 1(0), and B0-FillImp compensates for the hole _ with the inferred term N2(0). B0-InsertImp works basically the same as B0-FillImp, but it inserts the inferred term N2(0) rather than filling _. Some other rules also use terms conveyed by extended domains for elaboration. One can easily see that the shape of R(b) is determined basically in response to Ψ in each rule.

Due to the existence of B0-InsertImp, one may think that typing derivation is not syntax-directed and thereby uninterpretable as an algorithm. However, 𝒢Ψ0(0)R(0)N(0) can actually be read as an algorithm in the following ways, where (𝒢,Ψ,(0)) and (R(0),N(0)) are regarded as inputs and outputs, respectively: Suppose that, during the check of an expression (0), the traversal of a subexpression i(0) returned (Ri(0),Ni(0)).

  1. (i)

    If Ri(0) is of the form 𝐢𝐧𝐬𝐞𝐫𝐭{N(0):𝒯(0)}R(0), then, the sole possible rule used directly below is B0-InsertImp, and one can thereby replace the output with (R(0),(N(0)N(0))).

  2. (ii)

    Repeat the process (i) until Ri(0) is no longer of the form 𝐢𝐧𝐬𝐞𝐫𝐭{N(0):𝒯(0)}R(0). Then, one can use the rule corresponding to the form of (0).

That is, in a broad sense, typing rules are syntax-directed with respect to (0) and all the R(0)’s produced by its subexpressions, not just to (0).

𝒢Ψ0𝒯(0)<:R(0)

𝒢0N1(0):𝒯1(0)𝒢Ψ0[N1(0)/x]𝒯2(0)<:R2(0)𝒢Ψ,_0{x:𝒯1(0)}𝒯2(0)<:𝐟𝐢𝐥𝐥{N1(0):𝒯1(0)}R2(0)BI0-ImpGuess1

Ψ is neither of the form (Ψ,_) nor (Ψ,{N(0):𝒯(0)})𝒢0N1(0):𝒯1(0)𝒢Ψ0[N1(0)/x]𝒯2(0)<:R2(0)𝒢Ψ0{x:𝒯1(0)}𝒯2(0)<:𝐢𝐧𝐬𝐞𝐫𝐭{N1(0):𝒯1(0)}R2(0)BI0-ImpGuess2

𝒢𝒯1(0)𝒯1(0)N0(0)𝒢Ψ0[N0(0)N1(0)/x]𝒯2(0)<:R2(0)𝒢Ψ,{N1(0):𝒯1(0)}0{x:𝒯1(0)}𝒯2(0)<:{N0(0)/𝒯1(0)}R2(0)BI0-ImpGiven

𝒢𝒯1(0)𝒯1(0)N0(0)𝒢Ψ0[N0(0)N1(0)/x]𝒯2(0)<:R2(0)𝒢Ψ,(N1(0):𝒯1(0))00(x:𝒯1(0))𝒯2(0)<:(N0(0)/𝒯1(0))0R2(0)BI0-Arr

𝒢0𝒯(0)<:𝒯(0)BI0-Empty   𝒢Ψ1T(1)<:(Di)i=1mT(1)𝒢Ψ0T(1)<:(Di)i=1mT(1)BI0-Code

𝒢Ψ1T(1)<:R(1)

𝒢1T(1)<:T(1)BI1-Empty   T1(1)||1T1(1)𝒢Ψ1T2(1)<:R2(1)N(0):=T1(1)T1(1)𝒢Ψ,(T1(1))11T1(1)T2(1)<:(N(0)/T1(1))1R2(1)BI1-Arr

Figure 8: The declarative rules for the inference of implicit arguments.

The core of our inference lies in the judgments 𝒢Ψ0𝒯(0)<:R(0) and 𝒢Ψ1T(1)<:R(1). Figure 8 first shows the declarative rules for these judgments. Guided by the application context, BI0-Arr and BI0-ImpGiven instantiate the codomain type 𝒯2(0) by substituting x with (N0(0)N1(0)), i.e., the given argument wrapped by an appropriate cast function.

BI0-ImpGuess guesses an implicit argument N1(0); for actual implementation, we have to infer it algorithmically. The intuition for defining an algorithmic version is quite simple: we can track a finite set I of variables that should be resolved into a term through the traversal, and if about to insert casts for a type that contains unresolved variables and has some suitable structure, we can simply interpret that equation as a solution. For space reasons, the algorithmic rules are described in Figure 17 in the long version [63].

5 Basics of Horsea

To lessen the cumbersomeness of manually inserting staging constructs, we provide a proof-of-concept surface language named Horsea. Programs in this language are translated into λ{} by reconstructing staging constructs. This section explains the basics of how to properly complement and . The reconstruction process is formalized as an elaboration through type-checking-like traversal. The target language of the elaboration is λ{}, which we have introduced in Section 4. The formalization is fairly standard; we can reconstruct and by a well-known technique in the literature of partial evaluation called binding-time analysis (BTA) [39, 14, 15]. Because most part of the formalization is simply a repetition of classical results, we put precise descriptions only in Appendix C of the long version [63]. Nonetheless, care must be taken to handle dependent function types and implicit arguments in our case.

The syntax of source expressions E and type annotations T is quite concise:

E ::=dxλx:T.E(E1E2)λ{x:T}.E(E1{E2})E_
T ::=B{x:BE}𝚃𝚎𝚗𝚜𝚘𝚛E(x:T)T

As one can see, this is basically “λ{} without staging constructs”. Here, d ranges over the set of constants (e.g., 𝑚𝑎𝑡𝑀𝑢𝑙𝑡) associated with corresponding ones in λ{} (e.g., 𝑔𝑒𝑛𝑀𝑎𝑡𝑀𝑢𝑙𝑡).

BTA assigns a binding time b to every subexpression to produce e^ defined below:

e^::=ebe::=dxλx:τ^.e^(e^1e^2)λ{x:τ^}.e^(e^1{e^2})e^_
τ^::=τbτ::=B𝚃𝚎𝚗𝚜𝚘𝚛e^(x:τ^)τ^b::= 0 1

By the binding times ( stages) assigned to subexpressions, we can evidently reconstruct brackets and escapes by inserting them at each gap of the two binding times on the syntax tree, as described in Figure 21 in the long version [63]. The assignment can be done by extracting binding-time constraints from programs and solving them. For example, if the given non-staged program contains a subexpression (λx:𝙼𝚊𝚝m(n+𝟷).), then we can extract the information that variables m and n must be bound at stage 0 and that the type annotation 𝙼𝚊𝚝m(n+𝟷) must live in stage 1, in order to appropriately transform the program into the staged core language. Such constraints can be extracted in a type-checking-like manner, as explained in Appendix C of the long version [63].

6 Further Discussions

Although the formalization we have given so far basically satisfies our goal, we still have room for improvement for real-world use. This section briefly touch on some of such aspects.

6.1 Adding Conditionals Is Unexpectedly Non-Trivial But Viable

In addition to the constructs in our formalization, recursive functions and conditionals are desiderata for real-world use. While adding the former does not incur much difficulty as to typing, designing a rule for stage-0 𝐢𝐟-expressions is actually not as straightforward as expected. One may imagine that something like the following 𝐭𝐡𝐞𝐧-biased rule would work:

Γ0M0(0):𝙱𝚘𝚘𝚕N0(0)Γ0Mi(0):Ti(0)Ni(0)(for i{1,2})ΓT2(0)T1(0)N(0)Γ0(𝐢𝐟M0(0)𝐭𝐡𝐞𝐧M1(0)𝐞𝐥𝐬𝐞M2(0)):T1(0)𝐢𝐟N0(0)𝐭𝐡𝐞𝐧N1(0)𝐞𝐥𝐬𝐞N(0)N2(0)

However, this is not the actual way we want to check conditionals; for example, it cannot handle programs like the following:

𝐥𝐞𝐭𝐫𝐞𝐜𝑔𝑒𝑛𝑅𝑒𝑝(m:𝙽𝚊𝚝)(n:𝙽𝚊𝚝):𝚅𝚎𝚌%m𝚅𝚎𝚌%(mn)=λv:𝚅𝚎𝚌%m.
(𝐢𝐟n𝟶𝐭𝐡𝐞𝐧𝑣𝑒𝑐𝑁𝑖𝑙𝐞𝐥𝐬𝐞
(𝑔𝑒𝑛𝑉𝑒𝑐𝐶𝑎𝑡{m}{(n𝟷)m})v(𝑔𝑒𝑛𝑅𝑒𝑝m(n𝟷)v))

Here, 𝑣𝑒𝑐𝑁𝑖𝑙 is a built-in constant for the vector of length 0, and 𝑔𝑒𝑛𝑉𝑒𝑐𝐶𝑎𝑡 is the one that produces code of vector concatenation functions. The application 𝑔𝑒𝑛𝑅𝑒𝑝mn produces code of a function that takes a vector of length m and duplicate it n times to make the vector of length (mn). Because the 𝐭𝐡𝐞𝐧-branch and the 𝐞𝐥𝐬𝐞-branch have type T1(0):=𝚅𝚎𝚌%𝟶 and T2(0):=𝚅𝚎𝚌%(m+((n𝟷)m)), respectively, the inserted assertion T2(0)T1(0)L will pass only when either m or n equals 𝟶. This is clearly different from the intention; what we wanted to assert here is that both branches have type 𝚅𝚎𝚌%(mn).

This can be resolved by some rule that “merges” two types by conditionals as follows:

Γ0M0(0):𝙱𝚘𝚘𝚕N0(0)Γ0Mi(0):𝚅𝚎𝚌%Ni(0)Ni(0)(for i{1,2})Γ0(𝐢𝐟M0(0)𝐭𝐡𝐞𝐧M1(0)𝐞𝐥𝐬𝐞M2(0)):𝚅𝚎𝚌%(𝐢𝐟N0(0)𝐭𝐡𝐞𝐧N1(0)𝐞𝐥𝐬𝐞N2(0))𝐢𝐟N0(0)𝐭𝐡𝐞𝐧N1(0)𝐞𝐥𝐬𝐞N2(0)

Such merging can be generalized to arbitrary compatible pairs of types.

6.2 Handling Tensors with “Essentially Dynamic” Shapes

As we have seen so far, our language is basically designed so that all the computations will be specialized to certain tensor shapes at compile time. However, sometimes one wants programs to deal with tensors whose shapes are essentially unknown at compile time and available only at runtime. Indeed, it would be quite common, for example, to set up a server-side application that can receive image files ( matrices) of arbitrary sizes by request from users to perform some computation on them using tensors. To this end, with the aid of the 𝐫𝐮𝐧-primitive [66, 69, 31, 42], our language can also handle tensors with essentially dynamic shapes to some extent within its design principle. This is not completely free from runtime size mismatch, but even if a failure happens, it will be emitted immediately, not during actual tensor computation, and thus the user can still avoid wasting time.

The 𝐫𝐮𝐧-primitive is a special construct that can unlift code fragments like the following:

𝐥𝐞𝐭g(c:𝙸𝚗𝚝)=𝐫𝐮𝐧λx:𝙸𝚗𝚝.x+c𝐢𝐧g𝟺𝟸 57
0(𝐫𝐮𝐧λx:𝙸𝚗𝚝.x+𝟺𝟸) 570(λx:𝙸𝚗𝚝.x+𝟺𝟸) 570𝟿𝟿

Note that, in general, adding 𝐫𝐮𝐧 could require ingenious modification to the type system. This is because one cannot always unlift code fragments; even if the program is well-typed under naïve typing for staging, variables occurring in code fragments passed to 𝐫𝐮𝐧 might be locally unbound111111For example, the following term gets stuck: 𝐥𝐞𝐭h(t:𝙱𝚘𝚘𝚕)=𝟶𝐢𝐧λb:𝙱𝚘𝚘𝚕.(h(𝐫𝐮𝐧b)). . However, this can basically be solved by a method orthogonal to ours, and also, as explained later, we do not have to care too much about this in our setting.

The basic idea to achieve the dynamic feature by using 𝐫𝐮𝐧 is quite easy: when receiving some form of a tensor whose shape has not been fixed at compile time, we can (1) generate code of the necessary function specialized for its shape, (2) lift the tensor to the upper stage and embed it as an argument of the produced function, and (3) run the code to compute the final result. An implementation for doing this would be something like the following:

𝐥𝐞𝐭ℎ𝑎𝑛𝑑𝑙𝑒(𝑜𝑢𝑡:𝙵𝚒𝚕𝚎𝙿𝚊𝚝𝚑)(𝑑𝑦𝑛𝑀𝑎𝑡:𝙻𝚒𝚜𝚝(𝙻𝚒𝚜𝚝𝙸𝚗𝚝))=𝐦𝐚𝐭𝐜𝐡𝑟𝑒𝑐𝑡𝑆𝑖𝑧𝑒𝑑𝑦𝑛𝑀𝑎𝑡𝐰𝐢𝐭𝐡
|𝙽𝚘𝚗𝚎𝑝𝑟𝑖𝑛𝑡𝐸𝑟𝑟𝑜𝑟‘‘The given matrix is not rectangular’’
|𝚂𝚘𝚖𝚎(m,n)
𝐦𝐚𝐭𝐜𝐡𝐫𝐮𝐧(𝑓𝑜𝑟𝑔𝑒𝑡𝑆𝑖𝑧𝑒nm)((𝑔𝑒𝑛𝐹mn)(𝑙𝑖𝑓𝑡𝑀𝑎𝑡mn𝑑𝑦𝑛𝑀𝑎𝑡))𝐰𝐢𝐭𝐡
|𝙴𝚛𝚛𝚘𝚛𝑚𝑠𝑔𝑝𝑟𝑖𝑛𝑡𝐸𝑟𝑟𝑜𝑟(‘‘Bug: ’’++𝑚𝑠𝑔)
|𝙾𝚔𝑟𝑒𝑠𝑢𝑙𝑡𝑤𝑟𝑖𝑡𝑒𝑀𝑎𝑡𝑟𝑖𝑥𝑇𝑜𝐹𝑖𝑙𝑒𝑜𝑢𝑡𝑟𝑒𝑠𝑢𝑙𝑡

The function 𝑟𝑒𝑐𝑡𝑆𝑖𝑧𝑒:𝙻𝚒𝚜𝚝(𝙻𝚒𝚜𝚝𝙸𝚗𝚝)𝙾𝚙𝚝𝚒𝚘𝚗(𝙽𝚊𝚝𝙽𝚊𝚝) first takes a dynamic matrix and returns its size (m,n) wrapped by 𝚂𝚘𝚖𝚎 if it is rectangular (or returns 𝙽𝚘𝚗𝚎 otherwise). Then, by using this size, 𝑔𝑒𝑛𝐹 produces code of the function necessary for the user’s purpose, and 𝑙𝑖𝑓𝑡𝑀𝑎𝑡:(p:𝙽𝚊𝚝)(q:𝙽𝚊𝚝)𝙻𝚒𝚜𝚝(𝙻𝚒𝚜𝚝𝙸𝚗𝚝)𝙼𝚊𝚝%p%q lifts the matrix to code. As an example, we here suppose the case where the function produced by 𝑔𝑒𝑛𝑆𝑜𝑚𝑒𝐹𝑢𝑛 returns a matrix of the transposed size, so the type of 𝑔𝑒𝑛𝑆𝑜𝑚𝑒𝐹𝑢𝑛 is (p:𝙽𝚊𝚝)(q:𝙽𝚊𝚝)𝙼𝚊𝚝%p%q𝙼𝚊𝚝%q%p. After the main computation, we use 𝑓𝑜𝑟𝑔𝑒𝑡𝑆𝑖𝑧𝑒:(j:𝙽𝚊𝚝)(k:𝙽𝚊𝚝)𝙼𝚊𝚝%j%k𝙻𝚒𝚜𝚝(𝙻𝚒𝚜𝚝𝙸𝚗𝚝) so that the size of the matrix will be discarded from the type. Finally, by using 𝐫𝐮𝐧, we run the code constructed so far. Here, 𝐫𝐮𝐧 wraps resulting values with a sum type equivalent to OCaml’s 𝚛𝚎𝚜𝚞𝚕𝚝, and it will return an error message if some shape mismatch or unlifting failure has happened. Namely, we cannot perfectly eliminate the possibility of runtime shape mismatch after all, but the important point here is that, even if the implementation of 𝑔𝑒𝑛𝑆𝑜𝑚𝑒𝐹𝑢𝑛 causes a shape mismatch for the given size (or the occurrence of locally unbound variables), 𝐫𝐮𝐧 emit errors immediately for typical cases; such errors happen during code generation or unlifting, not during heavy computation involving tensors. Thus, owing to staging, one can still successfully avoid wasting time even if the program contains some shape mismatch.

7 Implementation Report

Based on the formalization we have given so far, we implemented a prototype type-checker in Haskell and made it publicly available [61, 62]. This type-checker accepts both programs in λ{} and those in Horsea; when receiving a program in Horsea, the type-checker internally converts it to the one in λ{} in a manner explained in Section 5.

To support realistic examples, the type-checker extends various aspects of the languages, such as some of the features mentioned in Section 6. Specifically, to handle example programs of ocaml-torch [9], an OCaml binding of PyTorch [50], we utilize stage-0 refinement types to express broadcasting [18, 13] of tensors. Broadcasting is a kind of implicit conversion of tensors, and the use of refinement types for this purpose is largely inspired by GraTen [32].

Our aims for implementing this are the following: (1) Because our method relies on the observation that tensor shapes are “not very dynamic” (i.e., that typical programs contain a limited number of operations where the resulting shape can be determined only at runtime), it is unclear whether our method sufficiently accommodates realistic programs. By porting examples offered by ocaml-torch [9] into our language, we substantiate the applicability of our method. (2) We demonstrate that our inference algorithm can reconstruct sufficiently many implicit arguments for the ported example programs. (3) While the core language is proven to be type-safe, its extension with implicit arguments and the surface language have yet to establish a mathematical guarantee (although they are heavily inspired by existing methods that fulfill type safety). By feeding various programs to the type-checker, we exemplify that the extensions indeed work fine.

Figure 9 displays the declaration of some built-in values, where val~x and valx bind x at stage 0 and 1, respectively, and &() denotes a bracket . Here, (M as T) is a construct for manually inserting assertions, i.e., works like the following121212The actual implementation as to 𝐚𝐬-expressions is, however, somewhat different from the rule shown here. It is rather something that enforces the so-called checking mode of bidirectional type-checking [20]. :

Γ0M1(0):T1(0)N1(0)Γ0S2(0)T2(0)ΓT1(0)T2(0)N0(0)Γ0(M1(0)𝐚𝐬S2(0)):T2(0)N0(0)N1(0)

The function broadcast takes two tensor shapes, and, if the two shapes are compatible (i.e., if tensors of the two shapes can be injected to one common shape), it returns that common shape, or raises a failure otherwise. Although having broadcast suffices for expressing broadcasting, expecting better error localization, we also provide broadcastable, a function that judges whether given two shapes are compatible, and use it in refinement predicates.

val ~broadcast : List Nat -> List Nat -> List Nat external ...
val ~broadcastable : List Nat -> List Nat -> Bool external ...
...
module Tensor = struct
val f : Float -> Tensor %[] external ...
val ~gen_zeros : (s : List Nat) -> &(Tensor %s) external ...
val ~gen_add : {x : List Nat} -> {y : {s : List Nat | broadcastable x s}} ->
&(Tensor %x -> Tensor %y -> Tensor %(broadcast x y)) external ...
val ~gen_grad : {s : List Nat} -> &(Tensor %s -> Tensor %s) external ...
val ~gen_zero_grad : {s : List Nat} -> &(Tensor %s -> Unit) external ...
val ~gen_mm : {a : Nat} -> {b : Nat} -> {c : Nat} ->
&(Mat %a %b -> Mat %b %c -> Mat %a %c) external ...
val ~gen_cross_entropy_for_logits :
{s : {v : List Nat | List.length v == 2}} ->
&(Tensor %s -> Tensor %[List.nth 0 s] -> Tensor %[]) external ...
...
end
module MnistHelper = struct
val ~image_dim = (28 * 28) as Nat
val ~num_train_images = 60000 as Nat
...
val train_images : Tensor %[num_train_images, image_dim] external ...
val train_labels : Tensor %[num_train_images] external ...
...
end
Figure 9: An excerpt of declarations about built-in functions.

Figure 10 shows an example program mnist/linear.hrs in Horsea, which is ported from an example program mnist/linear.ml offered by ocaml-torch [9]. This program tries linear regression for the well-known MNIST dataset [17]. Built-in functions used in this example are mapped to those of the core language like the following:

Tensor.(+)Tensor.gen_add,Tensor.mmTensor.gen_mm,
Tensor.cross_entropy_for_logitsTensor.gen_cross_entropy_for_logits,

Interestingly, the type-checker successfully reconstructs all the 20 implicit arguments in this program; several functions, such as + and mm, have implicit parameters, and our inference algorithm can compensate all of them, in combination with the interface of modules such as MnistHelper. Compared to the original program in OCaml, the essential differences are only three annotations highlighted by a bold blue typeface; simply adding these three suffices for tensor shape checking for this case. The first two are direct annotations for tensor shapes, and the last one, lift_int, is an annotation for BTA that turns compile-time integers into ones available at runtime as well.

let learning_rate = Tensor.f 1.0 in
let ws = Tensor.zeros (let open MnistHelper in [image_dim, label_count]) in
let bs = Tensor.zeros (let open MnistHelper in [label_count]) in
let model {n : Nat} (xs : Tensor [n, MnistHelper.image_dim]) =
let open Tensor in mm xs ws + bs
in
range 1 200 |> List.iter (fun(i : Int) ->
let loss =
Tensor.cross_entropy_for_logits
(model MnistHelper.train_images) MnistHelper.train_labels in
Tensor.backward loss;
Tensor.no_grad (fun(u : Unit) -> let open Tensor in
ws -= grad ws * learning_rate; bs -= grad bs * learning_rate);
Tensor.zero_grad ws; Tensor.zero_grad bs;
let got = model MnistHelper.test_images in
let estimated = Tensor.argmax 1 got in
let sum = Tensor.count_equal estimated MnistHelper.test_labels in
let test_accuracy = float sum / float (lift_int MnistHelper.num_test_images) in
print_float test_accuracy)
Figure 10: An example program in Horsea that can be checked by the prototype type-checker.

Table 1 shows similar results for 10 example programs (including mnist/linear.hrs). The columns “total” and “inferred” display the total number of implicit arguments in each program and the number of successfully inferred ones among those arguments, respectively. Those that cannot be inferred are manually specified in the programs (e.g., char_rnn/char_rnn.hrs contains 3937=2 manually specified implicit arguments, and the other 37 are appropriately reconstructed by the type-checker). We also show the number of shape-related descriptions contained in type annotations in each program on the column “#annot” because adding these descriptions often helps the inference.

As shown by these results, the inference algorithm works unexpectedly effective, despite its (intentionally) plain strategy; it can reconstruct approximately 90% of the implicit arguments. The major source of the inference failure is, on the other hand, the use of higher-order functions. It would thus be even better if we use Hindley–Milner-like unification-based algorithm. It might also be beneficial for the inference to compute function applications in types that are known to be pure and halting, such as those of broadcast, during type-checking.

Table 1: Properties of example programs and the inference results of implicit arguments.
program total inferred #annot #lines
char_rnn/char_rnn.hrs 39 37 12 118
gan/mnist_cgan.hrs 59 55 5 154
gan/mnist_dcgan.hrs 112 106 4 195
gan/mnist_gan.hrs 51 47 4 142
jit/load_and_run.hrs 5 3 0 17
min-gpt/mingpt.hrs 108 96 17 321
mnist/conv.hrs 28 25 3 72
mnist/linear.hrs 20 20 1 29
pretrained/finetuning.hrs 45 35 6 89
pretrained/predict.hrs 8 5 0 77

8 Related Work

8.1 Staged Computation

Typed languages with staging constructs, such as MetaML [66, 64, 67] or λ [14, 15], arose from the context of partial evaluation [48, 27, 28], and a considerable amount of studies have been done subsequently [16, 25, 65, 8, 74, 69, 31, 40]. Although they differ from one another in the design choice of constructs, many of them aim at ensuring the validity of produced code statically by checking code-generating programs. As to implementation, BER MetaOCaml [42, 43] extends OCaml with MetaML-style staging constructs, and Scala 3 [60] recently adopted a staging-based formalization for the core of its macro system.

Combining staging with types dependent on values in some sense seems to be investigated by somewhat limited number of studies. Concoqtion [23] has both indexed types and staging constructs, but their use is for establishing a tagless staged interpreter [51], and the language for computation and that for indices are separated. Kawata and Igarashi [40] propose λMD, a dependently-typed multi-stage language that can be regarded as a theoretical foundation. It also supports a kind of cross-stage persistence based on λ% [31]. Unlike our staged core language, λMD does not have some mechanism like stage-0 assertions T1(1)T2(1)L, and its type equality for checking function applications is based on the β-equivalence.

8.2 Tensor Shape Checking

Checking the consistency of programs as to tensor shapes is also a classical topic. The incorporation of length-indexed vector types into realistic languages dates back at least to Dependent ML [72, 71], and handling data-independent sizes of structures as type-level information has already been targeted by shapely types [38]. Since then, a variety of studies have been done to design a mechanism that is less general than full dependent types but handier in some sense for major use cases of tensor computation.

Repa [41] uses some kind of shape information at type level and supports shape polymorphism [56], i.e., the ability to reuse functions for some class of the shapes, by exploiting usual type classes, associated data types [10], and type families [57] offered by GHC [68]. Its main purpose of the use is, however, to achieve high-performance tensor computation while keeping high-level description in source programs at the same time, and seems not to pursue the complete elimination of shape mismatches. Accelerate [11] follows a Repa-based interface for its frontend of GPGPU programs. It also performs dynamic code generation for producing GPU kernel functions, so the utilization of our staging-based formalization for systems like Accelerate might be worth investigating.

To achieve strict safety, Gibbons [26] proposes an elegant embedded-DSL approach to checking shape consistency as to operations similar to those of APL [37, 35] by using Naperian functors, type classes, and the extension of GHC [68] for dependent types. This also supports rank polymorphism, a mechanism to allow conversions close to broadcasting [18, 13]. To support realistic programs, however, it would be good if tensor shapes are more “first-class,” as pointed out in Hattori et al. [32]. Henriksen and Elsman [33] and Bailly et al. [5] give another interesting formalization mainly for the use in Futhark [34]: a system with size-dependent types. While it may cause runtime errors due to array indexing or runtime coercion, it allows term-level variables for type-level indices and accommodates dynamically determined shapes by existential quantification on indices. Somewhat similar mechanisms are also proposed by Abe and Sumii [1] and Xi [71], the former of which exploits phantom types in OCaml. It might be worth considering to combine our work with such existential quantification to achieve better handling of tensors with dynamic shapes.

As another line of work, those based on refinement types [24, 22, 55, 44] or manifest contracts [21, 70, 30, 29, 58] are also prominent. Some early researches, such as hybrid type checking (λH) [22, 44] or liquid types [55], already mention array bounds checking as one of their applications. GraTen [32] propels this approach forward to support many tensor-related operations used in realistic DNN-related programs, such as examples of ocaml-torch [9], with a flavor of gradual typing [59]. Migeed, Reed, Ansel, and Palsberg [47] propose a less expressive gradually-typed system in order to strike a balance between the coverage of the verification and the affinity with the existing tool support. Our use of refinement types for stage-0 types is highly inspired by that of GraTen, although we provide shape-related functions like broadcast just as literally usual functions while GraTen handles them carefully so that the back-end SMT solver can ensure subtyping relations.

8.3 Bidirectional Type-Checking for Implicit Arguments

The idea of utilizing bidirectional type-checking [20] for reconstructing implicit arguments is not very new; Odersky et al. [49] give such a formalization as a foundation of Scala 3’s 𝐢𝐦𝐩𝐥𝐢𝐜𝐢𝐭-parameters. Due to the dependent nature of our formalization, however, we have found that extending Xie and Oliveira’s “let arguments go first” [73] better fits our purpose.

8.4 Distinction between Compile Time and Runtime

Although ours seems to give a staging-based foundation for tensor shape checking with a mathematical safety guarantee for the first time, the distinction between data available only at compile time (e.g., parameters for matrix sizes) and those at runtime (e.g., matrices themselves) by some other forms appears to have long been done by a number of existing studies. In particular, Idris 2 [7], which has a quite different core language from the previous version of Idris [6], adopts quantitative type theory (QTT) [46, 4] for distinguishing the two. Investigating the relationship between QTT-based type systems and staging-based ones, such as interoperability, might be another interesting topic. Other than that, LMS-Verify [2] proposes a method somewhat similar to ours from the operational perspective and provides an example of how to check size consistency of matrix operations, based on lightweight modular staging (LMS) [54] with a flavor of higher-order contracts [21]. It seems that, however, the consistency of generated code as to shapes is not guaranteed by language-level metatheory in this method; it is not the language (in particular, not the type system) but programmers who write contracts as to shapes that are responsible for ensuring such consistency.

9 Conclusion and Future Work

By utilizing staged computation, we have proposed a method that ensures the consistency as to tensor shapes through compile-time computation, aiming at an affinity with continuous development. We have also offered features for further reducing the burden of writing tensor-involving programs, such as implicit parameters or a non-staged surface language. Various future directions can be considered furthermore, as some of them are mentioned in Section 8, and others are as follows: (1) support basic type-related devices such as polymorphism or algebraic datatypes; (2) generalize λ to a multi-stage version and inspect the relationship between that language and λMD [40]; (3) improve elaboration rules so that fewer terms will be duplicated through assertion insertion; and (4) investigate the interoperability of our method with programs written in Idris or some dependently-typed languages.

References

  • [1] Akinori Abe and Eijiro Sumii. A simple and practical linear algebra library interface with static size checking. EPTCS, 198:1–21, 2014. doi:10.4204/EPTCS.198.1.
  • [2] Nada Amin and Tiark Rompf. LMS-Verify: abstraction without regret for verified systems programming. In Proceedings of the 44th ACM SIGPLAN Symposium on Principles of Programming Languages, POPL ’17, pages 859–873, New York, NY, USA, 2017. Association for Computing Machinery. doi:10.1145/3009837.3009867.
  • [3] Flavio Ascari, Roberto Bruni, Roberta Gori, and Francesco Logozzo. Sufficient incorrectness logic: SIL and Separation SIL, 2024. doi:10.48550/arXiv.2310.18156.
  • [4] Robert Atkey. Syntax and semantics of Quantitative Type Theory. In Proceedings of the 33rd Annual ACM/IEEE Symposium on Logic in Computer Science, LICS ’18, pages 56–65, New York, NY, USA, 2018. Association for Computing Machinery. doi:10.1145/3209108.3209189.
  • [5] Lubin Bailly, Troels Henriksen, and Martin Elsman. Shape-constrained array programming with size-dependent types. In Proceedings of the 11th ACM SIGPLAN International Workshop on Functional High-Performance and Numerical Computing, FHPNC 2023, pages 29–41, New York, NY, USA, 2023. Association for Computing Machinery. doi:10.1145/3609024.3609412.
  • [6] Edwin Brady. Idris, a general-purpose dependently typed programming language: Design and implementation. Journal of Functional Programming, 23:552–593, September 2013. doi:10.1017/S095679681300018X.
  • [7] Edwin Brady. Idris 2: quantitative type theory in practice. In Anders Møller and Manu Sridharan, editors, 35th European Conference on Object-Oriented Programming (ECOOP 2021), volume 194 of Leibniz International Proceedings in Informatics (LIPIcs), pages 9:1–9:26, Dagstuhl, Germany, 2021. Schloss Dagstuhl – Leibniz-Zentrum für Informatik. doi:10.4230/LIPIcs.ECOOP.2021.9.
  • [8] Cristiano Calcagno, Eugenio Moggi, and Tim Sheard. Closed types for a safe imperative MetaML. Journal of Functional Programming, 13(3):545–571, 2003. doi:10.1017/S0956796802004598.
  • [9] Jane Street Capital. ocaml-torch. https://github.com/janestreet/torch, 2023. Accessed: 2026-04-27.
  • [10] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones, and Simon Marlow. Associated types with class. In Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’05, pages 1–13, New York, NY, USA, 2005. Association for Computing Machinery. doi:10.1145/1040305.1040306.
  • [11] Manuel M.T. Chakravarty, Gabriele Keller, Sean Lee, Trevor L. McDonell, and Vinod Grover. Accelerating Haskell array codes with multicore GPUs. In Proceedings of the Sixth Workshop on Declarative Aspects of Multicore Programming, DAMP ’11, pages 3–14, New York, NY, USA, 2011. Association for Computing Machinery. doi:10.1145/1926354.1926358.
  • [12] Koen Claessen and John Hughes. QuickCheck: a lightweight tool for random testing of Haskell programs. In Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming, ICFP ’00, pages 268–279, New York, NY, USA, 2000. Association for Computing Machinery. doi:10.1145/351240.351266.
  • [13] PyTorch Contributors. Broadcasting semantics – PyTorch 2.6 documentation. https://pytorch.org/docs/stable/notes/broadcasting.html, 2024. Accessed: 2025-03-19.
  • [14] Rowan Davies. A temporal-logic approach to binding-time analysis. In Proceedings of the 11th Annual IEEE Symposium on Logic in Computer Science, LICS ’96, page 184, USA, 1996. IEEE Computer Society.
  • [15] Rowan Davies. A temporal logic approach to binding-time analysis. J. ACM, 64(1), 2017. doi:10.1145/3011069.
  • [16] Rowan Davies and Frank Pfenning. A modal analysis of staged computation. J. ACM, 48(3):555–604, 2001. doi:10.1145/382780.382785.
  • [17] Li Deng. The MNIST database of handwritten digit images for machine learning research. IEEE Signal Processing Magazine, 29(6):141–142, 2012.
  • [18] NumPy Developers. Broadcasting — NumPy v2.2 Manual. https://numpy.org/doc/stable/user/basics.broadcasting.html, 2024. Accessed: 2025-03-19.
  • [19] Dino Distefano, Manuel Fähndrich, Francesco Logozzo, and Peter W. O’Hearn. Scaling static analyses at Facebook. Commun. ACM, 62(8):62–70, 2019. doi:10.1145/3338112.
  • [20] Jana Dunfield and Neel Krishnaswami. Bidirectional typing. ACM Comput. Surv., 54(5), 2021. doi:10.1145/3450952.
  • [21] Robert Bruce Findler and Matthias Felleisen. Contracts for higher-order functions. In Proceedings of the Seventh ACM SIGPLAN International Conference on Functional Programming, ICFP ’02, pages 48–59, New York, NY, USA, 2002. Association for Computing Machinery. doi:10.1145/581478.581484.
  • [22] Cormac Flanagan. Hybrid type checking. In Conference Record of the 33rd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’06, pages 245–256, New York, NY, USA, 2006. Association for Computing Machinery. doi:10.1145/1111037.1111059.
  • [23] Seth Fogarty, Emir Pasalic, Jeremy Siek, and Walid Taha. Concoqtion: indexed types now! In Proceedings of the 2007 ACM SIGPLAN Symposium on Partial Evaluation and Semantics-Based Program Manipulation, PEPM ’07, pages 112–121, New York, NY, USA, 2007. Association for Computing Machinery. doi:10.1145/1244381.1244400.
  • [24] Tim Freeman and Frank Pfenning. Refinement types for ML. In Proceedings of the ACM SIGPLAN 1991 Conference on Programming Language Design and Implementation, PLDI ’91, pages 268–277, New York, NY, USA, 1991. Association for Computing Machinery. doi:10.1145/113445.113468.
  • [25] Steven E. Ganz, Amr Sabry, and Walid Taha. Macros as multi-stage computations: type-safe, generative, binding macros in MacroML. In Proceedings of the Sixth ACM SIGPLAN International Conference on Functional Programming, ICFP ’01, pages 74–85, New York, NY, USA, 2001. Association for Computing Machinery. doi:10.1145/507635.507646.
  • [26] Jeremy Gibbons. APLicative programming with Naperian functors. In Hongseok Yang, editor, Programming Languages and Systems, pages 556–583, Berlin, Heidelberg, 2017. Springer Berlin Heidelberg. doi:10.1007/978-3-662-54434-1_21.
  • [27] Robert Glück and Jesper Jørgensen. Efficient multi-level generating extensions for program specialization. In Proceedings of the 7th International Symposium on Programming Languages: Implementations, Logics and Programs, PLILPS ’95, pages 259–278, Berlin, Heidelberg, 1995. Springer-Verlag. doi:10.1007/BFB0026825.
  • [28] Robert Glück and Jesper Jørgensen. An automatic program generator for multi-level specialization. Lisp Symb. Comput., 10(2):113–158, 1997. doi:10.1023/A:1007763000430.
  • [29] Michael Greenberg. Manifest Contracts. PhD thesis, University of Pennsylvania, USA, 2013. AAI3609166.
  • [30] Michael Greenberg, Benjamin C. Pierce, and Stephanie Weirich. Contracts made manifest. In Proceedings of the 37th Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’10, pages 353–364, New York, NY, USA, 2010. Association for Computing Machinery. doi:10.1145/1706299.1706341.
  • [31] Yuichiro Hanada and Atsushi Igarashi. On cross-stage persistence in multi-stage programming. In Michael Codish and Eijiro Sumii, editors, Functional and Logic Programming, pages 103–118, Cham, 2014. Springer International Publishing. doi:10.1007/978-3-319-07151-0_7.
  • [32] Momoko Hattori, Naoki Kobayashi, and Ryosuke Sato. Gradual tensor shape checking. In Thomas Wies, editor, Programming Languages and Systems, pages 197–224, Cham, 2023. Springer Nature Switzerland. doi:10.1007/978-3-031-30044-8_8.
  • [33] Troels Henriksen and Martin Elsman. Towards size-dependent types for array programming. In Proceedings of the 7th ACM SIGPLAN International Workshop on Libraries, Languages and Compilers for Array Programming, ARRAY 2021, pages 1–14, New York, NY, USA, 2021. Association for Computing Machinery. doi:10.1145/3460944.3464310.
  • [34] Troels Henriksen, Niels G. W. Serup, Martin Elsman, Fritz Henglein, and Cosmin E. Oancea. Futhark: purely functional GPU-programming with nested parallelism and in-place array updates. In Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation, PLDI 2017, pages 556–571, New York, NY, USA, 2017. Association for Computing Machinery. doi:10.1145/3062341.3062354.
  • [35] International Organization for Standardization. ISO 8485:1989: Programming Languages – APL. Technical report, ISO, November 1989.
  • [36] International Organization for Standardization and International Electrotechnical Commission. ISO/IEC 14882:1998: Programming Languages – C++. Technical report, ISO/IEC, September 1998.
  • [37] Kenneth E. Iverson. A programming language. In Proceedings of the May 1-3, 1962, Spring Joint Computer Conference, AIEE-IRE ’62 (Spring), pages 345–351, New York, NY, USA, 1962. Association for Computing Machinery. doi:10.1145/1460833.1460872.
  • [38] C. Barry Jay and J. R. B. Cockett. Shapely types and shape polymorphism. In Donald Sannella, editor, Programming Languages and Systems – ESOP ’94, pages 302–316, Berlin, Heidelberg, 1994. Springer Berlin Heidelberg.
  • [39] Neil D. Jones, Carsten K. Gomard, and Peter Sestoft. Partial Evaluation and Automatic Program Generation. Prentice-Hall, Inc., USA, 1993.
  • [40] Akira Kawata and Atsushi Igarashi. A dependently typed multi-stage calculus. In Anthony Widjaja Lin, editor, Programming Languages and Systems, pages 53–72, Cham, 2019. Springer International Publishing. doi:10.1007/978-3-030-34175-6_4.
  • [41] Gabriele Keller, Manuel M.T. Chakravarty, Roman Leshchinskiy, Simon Peyton Jones, and Ben Lippmeier. Regular, shape-polymorphic, parallel arrays in Haskell. In Proceedings of the 15th ACM SIGPLAN International Conference on Functional Programming, ICFP ’10, pages 261–272, New York, NY, USA, 2010. Association for Computing Machinery. doi:10.1145/1863543.1863582.
  • [42] Oleg Kiselyov. The design and implementation of BER MetaOCaml. In Michael Codish and Eijiro Sumii, editors, Functional and Logic Programming, pages 86–102, Cham, 2014. Springer International Publishing.
  • [43] Oleg Kiselyov. MetaOCaml: Ten years later. In Jeremy Gibbons and Dale Miller, editors, Functional and Logic Programming, pages 219–236, Singapore, 2024. Springer Nature Singapore.
  • [44] Kenneth Knowles and Cormac Flanagan. Hybrid type checking. ACM Trans. Program. Lang. Syst., 32(2), 2010. doi:10.1145/1667048.1667051.
  • [45] Mark Lemay, Qiancheng Fu, William Blair, Cheng Zhang, and Hongwei Xi. A dependently typed language with dynamic equality. In Proceedings of the 8th ACM SIGPLAN International Workshop on Type-Driven Development, TyDe 2023, pages 44–57, New York, NY, USA, 2023. Association for Computing Machinery. doi:10.1145/3609027.3609407.
  • [46] Conor McBride. I got plenty o’ nuttin’. In Sam Lindley, Conor McBride, Phil Trinder, and Don Sannella, editors, A List of Successes That Can Change the World: Essays Dedicated to Philip Wadler on the Occasion of His 60th Birthday, pages 207–233. Springer International Publishing, Cham, 2016. doi:10.1007/978-3-319-30936-1_12.
  • [47] Zeina Migeed, James Reed, Jason Ansel, and Jens Palsberg. Generalizing shape analysis with gradual types. In Jonathan Aldrich and Guido Salvaneschi, editors, 38th European Conference on Object-Oriented Programming (ECOOP 2024), volume 313 of Leibniz International Proceedings in Informatics (LIPIcs), pages 29:1–29:28, Dagstuhl, Germany, 2024. Schloss Dagstuhl – Leibniz-Zentrum für Informatik. doi:10.4230/LIPIcs.ECOOP.2024.29.
  • [48] F. Nielson and R. H. Nielson. Automatic binding time analysis for a typed λ-calculus. In Proceedings of the 15th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’88, pages 98–106, New York, NY, USA, 1988. Association for Computing Machinery. doi:10.1145/73560.73569.
  • [49] Martin Odersky, Olivier Blanvillain, Fengyun Liu, Aggelos Biboudis, Heather Miller, and Sandro Stucki. Simplicitly: foundations and applications of implicit function types. Proc. ACM Program. Lang., 2(POPL), 2017. doi:10.1145/3158130.
  • [50] Adam Paszke, Sam Gross, Soumith Chintala, Gregory Chanan, Edward Yang, Zachary DeVito, Zeming Lin, Alban Desmaison, Luca Antiga, and Adam Lerer. Automatic differentiation in PyTorch. In NIPS-W, 2017.
  • [51] Emir Pašalić, Walid Taha, and Tim Sheard. Tagless staged interpreters for typed languages. In Proceedings of the Seventh ACM SIGPLAN International Conference on Functional Programming, ICFP ’02, pages 218–229, New York, NY, USA, 2002. Association for Computing Machinery. doi:10.1145/581478.581499.
  • [52] Benjamin C. Pierce. Types and Programming Languages. The MIT Press, 1st edition, 2002.
  • [53] Benjamin C. Pierce and David N. Turner. Local type inference. ACM Trans. Program. Lang. Syst., 22(1):1–44, 2000. doi:10.1145/345099.345100.
  • [54] Tiark Rompf and Martin Odersky. Lightweight modular staging: a pragmatic approach to runtime code generation and compiled dsls. In Proceedings of the Ninth International Conference on Generative Programming and Component Engineering, GPCE ’10, pages 127–136, New York, NY, USA, 2010. Association for Computing Machinery. doi:10.1145/1868294.1868314.
  • [55] Patrick M. Rondon, Ming Kawaguchi, and Ranjit Jhala. Liquid types. In Proceedings of the 29th ACM SIGPLAN Conference on Programming Language Design and Implementation, PLDI ’08, pages 159–169, New York, NY, USA, 2008. Association for Computing Machinery. doi:10.1145/1375581.1375602.
  • [56] Sven-Bodo Scholz. Single Assignment C: efficient support for high-level array operations in a functional setting. Journal of Functional Programming, 13(6):1005–1059, 2003. doi:10.1017/S0956796802004458.
  • [57] Tom Schrijvers, Simon Peyton Jones, Manuel Chakravarty, and Martin Sulzmann. Type checking with open type functions. In Proceedings of the 13th ACM SIGPLAN International Conference on Functional Programming, ICFP ’08, pages 51–62, New York, NY, USA, 2008. Association for Computing Machinery. doi:10.1145/1411204.1411215.
  • [58] Taro Sekiyama, Atsushi Igarashi, and Michael Greenberg. Polymorphic manifest contracts, revised and resolved. ACM Trans. Program. Lang. Syst., 39(1), 2017. doi:10.1145/2994594.
  • [59] Jeremy Siek and Walid Taha. Gradual typing for functional languages. In Scheme and Functional Programming, January 2006.
  • [60] Nicolas Stucki, Aggelos Biboudis, and Martin Odersky. A practical unification of multi-stage programming and macros. In Proceedings of the 17th ACM SIGPLAN International Conference on Generative Programming: Concepts and Experiences, GPCE 2018, pages 14–27, New York, NY, USA, 2018. Association for Computing Machinery. doi:10.1145/3278122.3278139.
  • [61] Takashi Suwa. Horsea. https://github.com/gfngfn/Horsea, 2025. Accessed: 2026-04-27.
  • [62] Takashi Suwa. Compile-time tensor shape checking via staged shape-dependent types (artifact), February 2026. doi:10.5281/zenodo.18501258.
  • [63] Takashi Suwa and Atsushi Igarashi. Compile-time tensor shape checking via staged shape-dependent types, 2026. Long version, Accessed: 2026-04-28. arXiv:2604.23807.
  • [64] Walid Taha. Multi-Stage Programming: Its Theory and Applications. PhD thesis, Oregon Graduate Institute of Science and Technology, 1999.
  • [65] Walid Taha and Michael Florentin Nielsen. Environment classifiers. In Proceedings of the 30th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’03, pages 26–37, New York, NY, USA, 2003. Association for Computing Machinery. doi:10.1145/604131.604134.
  • [66] Walid Taha and Tim Sheard. Multi-stage programming with explicit annotations. In Proceedings of the 1997 ACM SIGPLAN Symposium on Partial Evaluation and Semantics-Based Program Manipulation, PEPM ’97, pages 203–217, New York, NY, USA, 1997. Association for Computing Machinery. doi:10.1145/258993.259019.
  • [67] Walid Taha and Tim Sheard. MetaML and multi-stage programming with explicit annotations. Theoretical Computer Science, 248(1):211–242, 2000. doi:10.1016/S0304-3975(00)00053-0.
  • [68] GHC Team. Glasgow Haskell Compiler. https://www.haskell.org/ghc/. Accessed: 2026-04-27.
  • [69] Takeshi Tsukada and Atsushi Igarashi. A logical foundation for environment classifiers. In Pierre-Louis Curien, editor, Typed Lambda Calculi and Applications, pages 341–355, Berlin, Heidelberg, 2009. Springer Berlin Heidelberg. doi:10.1007/978-3-642-02273-9_25.
  • [70] Philip Wadler and Robert Bruce Findler. Well-typed programs can’t be blamed. In Giuseppe Castagna, editor, Programming Languages and Systems, pages 1–16, Berlin, Heidelberg, 2009. Springer Berlin Heidelberg. doi:10.1007/978-3-642-00590-9_1.
  • [71] Hongwei Xi. Dependent ML an approach to practical programming with dependent types. J. Funct. Program., 17(2):215–286, 2007. doi:10.1017/S0956796806006216.
  • [72] Hongwei Xi and Frank Pfenning. Eliminating array bound checking through dependent types. In Proceedings of the ACM SIGPLAN 1998 Conference on Programming Language Design and Implementation, PLDI ’98, pages 249–257, New York, NY, USA, 1998. Association for Computing Machinery. doi:10.1145/277650.277732.
  • [73] Ningning Xie and Bruno C. d. S. Oliveira. Let arguments go first. In Amal Ahmed, editor, Programming Languages and Systems, pages 272–299, Cham, 2018. Springer International Publishing. doi:10.1007/978-3-319-89884-1_10.
  • [74] Yosihiro Yuse and Atsushi Igarashi. A modal type system for multi-level generating extensions with persistent code. In Proceedings of the 8th ACM SIGPLAN International Conference on Principles and Practice of Declarative Programming, PPDP ’06, pages 201–212, New York, NY, USA, 2006. Association for Computing Machinery. doi:10.1145/1140335.1140360.