Compile-Time Tensor Shape Checking via Staged Shape-Dependent Types
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 , 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 checkingCopyright and License:
2012 ACM Subject Classification:
Software and its engineering Functional languages ; Software and its engineering Software verification and validation ; Theory of computation Type structuresAcknowledgements:
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.14Editors:
Robbert Krebbers and Alexandra SilvaSeries and Publisher:
Leibniz International Proceedings in Informatics, Schloss Dagstuhl – Leibniz-Zentrum für Informatik
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 that takes three matrices , , and and computes , where is a binary operator that vertically concatenates two matrices. For verifying consistency, the following constraints must be met: (1) for the use of , and must have the same number of columns; and (2) for the matrix multiplication, the number of ’s columns must be equal to the number of ’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 or , which are the type for vectors of length and the one for matrices of size , respectively, and assign the matrix multiplication the following type: . Other methods, such as GraTen [32], offer refinement types like or . As an example of this kind of approach, consider the function above implemented in an Idris-like hypothetical language:
Here, are binders for implicit parameters, and and stand for the matrix multiplication and , which are assigned types and , respectively. is a manually proved lemma used for matching ’s actual type with the one required of ; since is of type and the function requires its argument to be of for some , the type-checker must verify that holds. Working as a proof, the lemma rewrites the former type to the latter through the -construct. In general, to type-check an application , where and are known to have types and , respectively, we must prove the type equality , i.e., that, for , argument expressions and 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.
The externality of requirements: Requirements imposed on software continuously arise due to social situations and users’ preferences, which are not predictable beforehand.
-
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.
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:
-
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.
-
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.
-
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. .
-
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 along with the third dimension and the latter by , 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 , 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 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 (called bracket) and (called escape) correspond to (hygienic) quasiquotation and splicing in Lisp, respectively. Intuitively, a bracket evaluates to a code value: for example, evaluates to itself, which stands for a piece of code that performs addition of 1 and 4. An escape is supposed to appear inside a bracket; when is evaluated to a code value , the code is spliced into the surrounding code: For example, 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. :
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, takes , , , and as stage- parameters, and produces code for specialized for those parameters. Here, takes three parameters , , and and returns , where is the specialized operation that vertically concatenates and matrices. The stage- built-in function also takes three parameters , , and and returns , where is the matrix multiplication operation specialized for and (if , , and 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 by combining dependent function types and code types , meaning that , , and are available at stage and the resulting code for matrix computation is at stage . By the same token, has type .
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- casts of the form 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:
In this case, to identify and to ensure the validity of the use of , the cast 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, and – are equal using the concrete values of and . Thus, if is applied to concrete integers, say, , , , and , the expression will evaluate to the following code without failure:
The body of the bracket is basically given type 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 ) suffices555The types of primitives are also simple. For example, is given . .
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- 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 ). 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.
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- (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- function parameters can be described by types for the sake of error localization.
-
2.
Extension with implicit arguments and their reconstruction rules: To alleviate the burden of specifying shape-related stage- 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.
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.
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:
-
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.
-
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).
-
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.
-
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.
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 3–5.
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 as follows:
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 , a variant form of stage- dependent function types. Functions of this type work exactly the same as those of type from the operational perspective, but they allow users to omit arguments and require the type-checker to infer an expression that should substitute for each context. For example, is now assigned type , meaning , , and can be implicit. Users can also define a function with implicit arguments by using variant -abstractions of the form . In addition, when one wants to specify arguments explicitly for implicit parameters, applications of the form can be used.
The reconstruction of implicit arguments can be done in a type-guided manner. For example, consider the subexpression , and let , , and denote expressions that should be reconstructed for the three parameters. According to the type assigned to , the reconstructed subexpression will have type . Since this subexpression is applied to , which has type , the domain type must be identical to after evaluation. In this case, we can infer that it suffices to just substitute and with and , respectively. Similarly, by comparing with the type of (i.e., ), we can judge that can be used as . 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:
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 , stage- 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 . This will cause an error with the position in the definition of at which is applied to , i.e., the first parameter of . However, it is the call site of passing , not the definition of , that this error should be attributed to. It will be better if we have a mechanism to emit a failure when is applied to a negative value like .
To this end, we use refinement types [24, 22, 55, 44] of the form for stage-0 types. The annotations can be modified from to , which abbreviates :
Then, consider an application . Now that the type for tracks the precondition required of , the above application elaborates to (assuming elaborates to ), where is a cast function to assert that the argument satisfies the predicate of the refinement type , and is a label that points to the original application. This will emit a failure if 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 , 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 , 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:
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, 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 in Horsea as follows:
As one can see, thanks to the omission of some stage- 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 and , which range over the set of stage- expressions and that of stage- type annotations, respectively (for ):
Here, and respectively range over the set of built-in functions available only at stage (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 may be restricted by refinement predicates (e.g., one cannot pass to ), must be simply-typed. Each constant ranged over by or has its own arity, and in particular, base constants are stage-agnostic constants of arity . We denote these arities by and . 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- tensor types have a stage- expression 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- binders of the form will be after compile-time computation and thereby that every tensor in generated code has a specialized shape. Stage- types and for vectors and matrices can be provided as syntax sugar of and , 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- tensor types have fixed shapes; the metavariable 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- tensor types occur mostly in stage- terms obtained by unlifting generated code and are seldom written by users.
In response to the setting of stage- tensor types, stage- function types can be dependent ones. This allows, for instance, to be assigned . However, this does not apply to stage ; by restricting stage- 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 for , where .
The target syntax is basically an “assertion-included” variant of the source syntax. It consists of stage- assertive terms and assertive types defined by the following:
Stage- assertions have two forms. The first one is , which judges that the stage- types and syntactically coincide after evaluation. Interestingly, adding assertions of this form covers all the necessary cases for checking type equality itself. The attachment stands for the source of errors when the assertion fails. The other one is used for checking the validity of downcasts. In operational terms, it basically works as an identity function, but before returning the argument (say ) as is, it checks whether the argument satisfies the predicate , i.e., whether evaluates to . The variant called an active check is the intermediate form of this assertion process, where is a “refinement proposition” (i.e., the application of the refinement predicate to the tested value ) under evaluation. It also keeps separately so that it will evaluate to 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].
S0-Brkt S0-Cst0
S0-CstP S0-Abs
S0-App S0-Var
S1-Esc S1-CstP S1-Var
S1-Abs
S1-App
ST0-Base
ST0-Tensor ST1-Base
ST0-Arr ST0-Code
ST1-Arr ST1-Tensor
Typing judgments are defined as (for ), which can be read as “under the type environment , the source expression has type and elaborates to by assertion insertion.” The structure of type environments are defined by: , 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 . Also, for constants, we have S0-Cst0, S0-CstP, and S1-CstP. Here, we use two environments and ; the former maps stage--specific built-in functions to stage- types , and the latter works similarly for stage-agnostic constants . The metavariable and its unlifting will be introduced shortly. Entries are like the following:
I-Tensor
I-Rfn
I-Code
I-Arr
The most distinctive typing rules are those for applications, i.e., S0-App and S1-App. Here, judges type compatibility, i.e., type equivalence ignoring the difference of argument expressions, and generates assertive cast terms . 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 . 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 [58].
E0-App1 E0-App1F
E0-Ass1 E0-AssFail
E0-AssPass E0-Beta
E0-Delta E0-RfnStart
E0-Brkt E0-RfnAct
E0-RfnPass E0-RfnFail
E1-App1 E1-App1F
E1-Abs1 E1-Abs2
E1-Esc E1-EscF E1-Cancel
ET1-TensorF ET1-Arr1F
ET1-Tensor ET1-Arr1
Assertive terms are equipped with staged call-by-value small-step reduction relations 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 standing for failures, and then the result is propagated to the whole program. Figure 4 displays the rules for this operational semantics, where stage- values (for ) and stage- type values are defined by the following:
Intuitively, and correspond to “completed” code fragments and type annotations, respectively. Here, ranges over the set of runtime constants, which consists of base constants and possibly partially applied built-in functions (e.g., or ). More formally, ranges over the following: (i) with (i.e., base constants), (ii) such that , and (iii) such that . Note that, unlike stage- type expressions, we do not have to evaluate stage- type expressions; while stage- types should remain in produced code (possibly for further post-process optimization), stage- types are all thrown away by -reduction through stage- 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 . Entries of are like the following:
As an abuse of notations, we often write for (resp. ) when (resp. ).
When code generation successfully terminates with a code value , such can be regarded as a stage- term by unlifting operations and :
One can then evaluate as ordinary runtime execution, which will not cause any failure since 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 is a closed term, for example. This restriction is crucial for our metatheory, specifically for proving cotermination [58].
3.2 Metatheory
T0-RfnPred
T0-Var
T0-App
T0-Ass
T0-CstP
T0-TyEquiv
T0-Cst0
T0-Abs
T0-Brkt
T0-Rfn
T1-Esc
T0-RfnAct
T1-App
T1-Abs
T1-CstP
T1-TyEquiv
T1-Var
For the purpose of proving type safety, assertive terms are also assigned types by declarative target typing of the form . Figure 5 displays the rules for these judgments. Some of the rules depend on type equivalences and well-formedness judgments and defined in Figures 13 and 14 in the long version [63]. The equivalences 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 , then .
Proving Preservation and Progress [52] is much more challenging due to the combination of the -reduction, the type equivalences , and the dependent nature of our typing. To prove Preservation as to built-in functions, we must assume some natural properties on , , and . For example, since holds, and must have equivalent types. However, we cannot use the type equivalences to describe such assumptions; because the definition of 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 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 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 means that all the entries in are of the form :
Theorem 2 (Preservation).
If and , then .
Theorem 3 (Progress).
If and , then we have one of the following: (1) ; (2) there exists such that ; or (3) is a value.
G-Var
G-Abs
G-Cst
G-App
For proving the safety of generated code, we use an additional type judgment defined by the rules in Figure 6, where is defined by: . Unlike the target typing, this type system uses only value types 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 (, , , and so on). The following states that generated code is always well-typed under the -rules, where is the evident pointwise equivalence based on :
Lemma 4.
If , , and , then there exists such that and .
However, there is one remaining challenge as to proving this lemma; the following property is crucial (since the -rules are independent of ):
Lemma 5.
implies .
We prove this by separating it into two: (1) implies , where is the standard -equivalence; and (2) implies . While the former is straightforward, the latter requires slight attention: the reduction includes the rule , which is not left-linear in the sense that the metavariable 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.
implies .
Since unlifted code does not contain 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 and , then is of the form , and this satisfies one of the following: (1) there exists such that , or (2) the evaluation of 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- 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., ). 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. :
The construct works as -abstractions whose parameter can be implicit, and can be used to specify an argument for such parameters explicitly. The form lies between complete omission and explicit designation; it indicates the existence of ’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 .
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.
B0-Var
B0-App
B0-AbsAnnot1
B0-Brkt
B0-AppImp
B0-FillImp
B0-InsertImp
B1-Esc
B1-Var B1-CstP
B1-App
BT1-Tensor
BT0-Code BT0-Imp
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- expressions are (for ), which can be understood as “under the type environment and the application context , the expression is well-typed and can be elaborated to the target term , and its type is instantiated to 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 . Type environments used here are normal entities that just associate stage- variables with stage- types for , respectively: . The sole notable thing is that, while we continue using for stage- types, the syntax of stage- types is extended with those for functions with implicit parameters as follows:
Some rules in Figure 7 uses the evident injection of -types to -types, which simply forgets the difference between and .
One of the essential devices to do “let arguments go first” is application contexts . When checking an expression 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 :
An entry (resp. ) stands for the existence of a stage- mandatory argument (resp. an explicitly specified argument for an implicit parameter) of type whose elaboration result is . 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 represents the existence of and a stage- argument of type , 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 . This instantiates to an extended return type so that the domain types can match the types of the arguments passed to the variable. B1-Var and do basically the same thing for stage- variables. The syntax of and is defined by the following:
As the name suggests, extended return types basically work as types; can be seen as a function type just by regarding the extended domain as or . 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 returned by the traversal of , and B0-FillImp compensates for the hole with the inferred term . B0-InsertImp works basically the same as B0-FillImp, but it inserts the inferred term rather than filling . Some other rules also use terms conveyed by extended domains for elaboration. One can easily see that the shape of 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, can actually be read as an algorithm in the following ways, where and are regarded as inputs and outputs, respectively: Suppose that, during the check of an expression , the traversal of a subexpression returned .
-
(i)
If is of the form , then, the sole possible rule used directly below is B0-InsertImp, and one can thereby replace the output with .
-
(ii)
Repeat the process (i) until is no longer of the form . Then, one can use the rule corresponding to the form of .
That is, in a broad sense, typing rules are syntax-directed with respect to and all the ’s produced by its subexpressions, not just to .
BI0-ImpGuess1
BI0-ImpGuess2
BI0-ImpGiven
BI0-Arr
BI0-Empty BI0-Code
BI1-Empty BI1-Arr
The core of our inference lies in the judgments and . Figure 8 first shows the declarative rules for these judgments. Guided by the application context, BI0-Arr and BI0-ImpGiven instantiate the codomain type by substituting with , i.e., the given argument wrapped by an appropriate cast function.
BI0-ImpGuess guesses an implicit argument ; 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 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 and type annotations is quite concise:
As one can see, this is basically “ without staging constructs”. Here, ranges over the set of constants (e.g., ) associated with corresponding ones in (e.g., ).
BTA assigns a binding time to every subexpression to produce defined below:
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 , then we can extract the information that variables and must be bound at stage and that the type annotation must live in stage , 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- -expressions is actually not as straightforward as expected. One may imagine that something like the following -biased rule would work:
However, this is not the actual way we want to check conditionals; for example, it cannot handle programs like the following:
Here, is a built-in constant for the vector of length , and is the one that produces code of vector concatenation functions. The application produces code of a function that takes a vector of length and duplicate it times to make the vector of length . Because the -branch and the -branch have type and , respectively, the inserted assertion will pass only when either or equals . This is clearly different from the intention; what we wanted to assert here is that both branches have type .
This can be resolved by some rule that “merges” two types by conditionals as follows:
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:
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: . . 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 function first takes a dynamic matrix and returns its size 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 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 . After the main computation, we use 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- 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 and bind at stage and , respectively, and denotes a bracket . Here, 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]. :
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.
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:
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.
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 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.
| 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 , 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, does not have some mechanism like stage- assertions , 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 () [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- 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 [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.
