Lectures onType Theory
Chapter 139
Chapter 139Optional

Typed Functional Array Languages: Shapes, Uniqueness, and Parallel Compilation

Prerequisites. Direct starred prerequisites: Chapter 19 and chapter 133; chapter 31 supplies the indexed families. No later core chapter depends on this route.

A dot product takes two vectors and multiplies them componentwise before summing. In an ordinary functional language its type is []int[]intint, which says that both arguments are arrays of integers and nothing else. It does not say that the two lengths agree, so the componentwise multiplication has no static justification and a run-time check must decide the matter.

Change one feature. A filter returns the elements satisfying a predicate; its result length is not any expression the caller can write down, because it depends on the values in the input. A type that must name every length cannot type filter at all.

Change one more. A nested map over an array of arrays exposes two levels of parallelism, and a flat accelerator target must recover the inner level without changing the result. Whether that is possible depends on whether the inner arrays all have the same length — which is again a fact about sizes.

Three obstacles, one object: a type language in which sizes are expressions. This chapter freezes the language F of Bailly, Henriksen and Elsman (2023), proves its soundness proposition at exactly the strength that paper states, and only then compares two implementations.

The frozen language F

Definition 139.1 — Syntax

e::=nxxeeler(e1,e2)fst esnd ee[e]iota emap ef eaif ec then et else efλ(y:τ).ee eeτlet [x] y:τ=e in elet y [z]:τ=e in e,τ::=int(τ1,τ2)[e]τ(x:τ)μ,μ::=τx.μ,σ::=τz.τ,v::=n(v1,v2)[v1,,vn] (n1)x,e,ρ. The type [e]τ is the type of arrays of e elements of type τ, where e is an expression of type int. A return type μ may bind existential sizes; a scheme σ may bind universal ones. Arrays are nonempty by the value grammar.

Definition 139.2 — Witnessed sizes

wit(int)=,wit((τ1,τ2))=wit(τ1)wit(τ2),wit([x]τ)={x}wit(τ),wit([e]τ)=wit(τ) (e not a variable),wit(x.τ)=wit(τ){x},wit((z:τ)μ)=. An existential size in a return type must be witnessed by the underlying type.

The witness condition is the chapter’s first non-obvious clause and it is worth saying at once what it excludes. If a value had type [fx]τ for some function f, recovering x from the value would require inverting f. Definition 139.2 does not let x count as witnessed there, so no rule ever asks for that inversion.

Definition 139.3 — Typing

Γn:int
t-int
Γ(x)=τ
Γx:τ
t-var
Γ(x)=z.τΓe:int
Γxe:τ[e/z]
t-inst
Γe:μΓμ<:μ
Γe:μ
t-relax
Γe:[es]τΓe:int
Γe[e]:τ
t-index
Γe:int
Γiota e:[e]int
t-iota
Γef:(x:τ)τxfv(τ)Γea:[es]τ
Γmap ef ea:[es]τ
t-map
Γ,y:τe:μΓτ ok
Γλ(y:τ).e:(y:τ)μ
t-lam
Γe:(x:τ)μΓe:τ
Γe e:μ[e/x]
t-app
Γe:τΓτ okτsτ
Γeτ:τ
t-coerce
Γe:x.τΓ,x:int,y:τe:μxwit(τ)x,yfv(μ)
Γlet [x] y:τ=e in e:μ
t-let
Γ,z:inte:τΓ,y:z.τe:μzfv(τ)yfv(μ)
Γlet y [z]:τ=e in e:μ
t-let-gen

Subtyping Γμ<:μ replaces an expression appearing as a size by a fresh existentially bound size variable; it is structural, contravariant in function domains, and throws away size information only. The relation τsτ in t-coerce is structural equality up to sizes.

Two clauses of definition 139.3 carry the design. t-app substitutes the argument into the result type, as in any dependent system. t-map does not: its side condition xfv(τ) forbids the element type from depending on the element, which is what keeps the result a regular array rather than a ragged one.

Definition 139.4 — Dynamic semantics

Evaluation is big-step, ρev, with the expected rules; the three that involve sizes are

ρenn>0
ρiota e[0,,n1]
d-iota
ρevρvτ
ρeτv
d-coerce
ρevτxvnρ,x:n,y:vev
ρlet [x] y:τ=e in ev
d-let

Dynamic size matching τxvn extracts a size from a value — [x]τx[v1,,vn]n, and a pair searches its components — and returns the marker when x is not witnessed. Dynamic size checking ρvτ verifies that every array in v has the length its type names.

Remark 139.5 — What the semantics does not model

Definition 139.4 defines successful executions only. A failed size coercion, an out-of-bounds index, and an attempt to build an array of fewer than one element all have no rule, so they have no derivation — not a derivation producing an error. Everything proved below is therefore conditioned on the existence of a successful evaluation derivation.

Value equivalence and substitution

Definition 139.6 — Value equivalence

vvv is the equivalence relation with nvn, componentwise on pairs and arrays, and, on closures, x,e,ρvx,e,ρiffρ,x:vev1 implies ρ,x:vev2with v1vv2, and symmetrically.

Two closures are equivalent when they return equivalent values on the same argument and fail on the same arguments. The second half is what makes the relation usable in a semantics that models only successful executions.

Proposition 139.7 — Substitution preserves semantics

  1. If ρ,x:v0ev and ρev0 then ρe[e/x]v with vvv.

  2. If ρ,x:v0vτ and ρev0 then ρvτ[e/x].

  3. Conversely for both, and correspondingly for the logical relation of definition 139.11.

Proof of Proposition 139.7 — Substitution preserves semantics

Proof. Clauses 1 and 2 are proved by mutual induction on the evaluation and size-checking derivations; the mutual induction is forced because d-coerce appeals to size checking and size checking appeals to evaluation in c-arr. The converses are proved by mutual induction on the structure of e rather than on a derivation, because the hypothesis is about e[e/x] and gives no derivation for e. The last clause follows from the first three. Equivalence rather than equality appears in the conclusion because substituting a term for a variable duplicates the term’s evaluation, and two evaluations of one closure need not produce syntactically identical environments. ◻

Exercise 139.1

★★☆ Give two closures that are equivalent by definition 139.6 but not syntactically identical, and show that clause 1 of proposition 139.7 would be false with equality in place of v.

Calculating with sizes

The type language is now fixed; the following calculations expose one clause of it at a time.

Example 139.8 — Dot product

Take zip:z.([z]int)([z]int)[z](int,int) and sum:z.([z]int)int. Then λ(a:[n]int).λ(b:[n]int).sum n (map (λ(p:(int,int)).fst p×snd p) (zip n a b)) has type ([n]int)([n]int)int. The t-map side condition holds because the result element type int does not mention p. A caller applying this to arrays of different declared lengths has no derivation: t-app substitutes the first argument’s size into the second argument’s type, and the second argument’s type must match syntactically.

Example 139.9 — Concatenation and reshape

concat:z1.z2.([z1]τ)([z2]τ)[z1+z2]τ requires the size language to contain arithmetic, and it does: sizes are expressions. Two arrays whose lengths are n and m concatenate at [n+m]τ, and [n+m]τs[m+n]τ, so a coercion converts between them — but only by t-coerce, which inserts a run-time check. Syntactic size equality does not know that addition is commutative.

Example 139.10 — Filter and the escaping size

filter cannot be given the type ([n]τ)[m]τ for any expression m the caller can write. Its type is ([n]τ)k.[k]τ: the result size is existentially bound. The only way to use the result is t-let, which unpacks the existential, binds k as an integer variable, and requires kwit(τ) so that d-let can extract its value from the array by dynamic size matching. The side condition kfv(μ) stops the size from escaping into the result type of the let.

Exercise 139.2

★★☆ Write a program that filters an array and then takes the length of the result, and check that it type-checks. Then write one that filters and returns the result itself from a function whose declared result type names the length, and identify the side condition of t-let that fails.

Exercise 139.3

★★☆ Delete xfv(τ) from t-map and give a program whose type is then a ragged array type. Say what the corresponding value would have to look like and why definition 139.1 has no such value.

Soundness

Definition 139.11 — The logical relation

ρv:μ is defined by ρn:int always;ρ(v1,v2):(τ1,τ2) iff ρvi:τi;ρ[v1,,vn]:[e]τ iff ρen and ρvi:τ for all i;ρx,e,ρ:(x:τ)μ iff, for all v1,v2,if ρv1:τ and ρ,x:v1ev2 then ρ,x:v1v2:μ;ρv:x.μ iff ρ,x:nv:μ for some n;ρv:z.τ iff ρv:(z:int)τ. Extend pointwise: ρΓ iff the domains agree, Γ ok, and ρρ(x):Γ(x) for every x.

The array clause is where the relation stops being a syntactic predicate: it evaluates the size expression in ρ and compares the result with the actual length. That is why the relation is indexed by a dynamic environment at all.

Proposition 139.12 — Extensibility

Write ρXρ when ρ(x)vρ(x) for every xX. Then evaluation, size checking, the logical relation at τ, at μ, at σ, and at a context are all invariant under replacing ρ by any ρ agreeing with it on the relevant free variables.

Proposition 139.13 — Dynamic size matching and checking

  1. If ρv:x.τ and xwit(τ) then τxvn for some n; and conversely, if some n is extracted then xwit(τ).

  2. If ρv:x.τ and τxvn then ρ,x:nv:τ.

  3. If ρvτ then ρv:τ.

Proof of Proposition 139.13 — Dynamic size matching and checking

Proof. Clause 1 is by induction on τ, following definition 139.2: the only clause contributing a witness is [x]τ, and there the array’s length supplies n. Clause 2 is by induction on the structure of τ. Clause 3 is by induction on the size-checking derivation, each of whose rules is the corresponding clause of definition 139.11. Clause 1 uses that arrays are nonempty: for an empty array every τ would satisfy ρv:[0]τ, while extracting a size from [0][x]τ would require the array’s first element. ◻

Proposition 139.14 — Value subtyping

If ρΓ, Γμ<:μ and ρv:μ, then ρv:μ.

Theorem 139.15 — Soundness

If Γe:μ, ρΓ and ρev, then ρv:μ.

Proof of Theorem 139.15 — Soundness

Proof. By induction on the typing derivation, with proposition 139.14 for t-relax.

t-int, t-var. Immediate from definition 139.11 and ρΓ.

t-app. Inverting d-app, the function evaluates to a closure x,e0,ρ and the argument to v, and ρ,x:ve0v. The induction hypothesis at the function gives ρx,e0,ρ:(x:τ)μ, and at the argument gives ρv:τ. Unfolding the closure clause of definition 139.11 at those data gives ρ,x:vv:μ, and clause 5 of proposition 139.7 converts that to ρv:μ[e/x], which is the conclusion of t-app.

t-map. Inverting d-map, the array evaluates to [v1,,vn] and each vi is the closure’s result on vi. The induction hypothesis at ea gives ρesn and ρvi:τ; the hypothesis at ef gives ρ,x:vivi:τ, and xfv(τ) with proposition 139.12 strips the extra binding, giving ρvi:τ. Hence ρ[v1,,vn]:[es]τ. Without the side condition the n results would be related at n different types and no array type would hold.

t-let. Inverting d-let, e evaluates to v, the sizes n are extracted by dynamic size matching, and e evaluates in the extended environment. The hypothesis at e gives ρv:x.τ; clause 2 of proposition 139.13 gives ρ,x:nv:τ, so the extended environment is related to the extended context and the hypothesis at e applies. The side condition x,yfv(μ) and proposition 139.12 strip the extra bindings from the conclusion.

t-coerce. Inverting d-coerce gives ρvτ, and clause 3 of proposition 139.13 gives ρv:τ directly — the induction hypothesis at e is not needed.

The remaining rules are componentwise. ◻

Remark 139.16 — The exact strength of thm:arr-soundness

The theorem quantifies over a successful evaluation: its third hypothesis is a derivation of ρev. It therefore proves

  • no progress statement — it does not say that a well-typed expression has an evaluation derivation;

  • no termination statement — an expression with no derivation may be diverging or failing, and the theorem does not distinguish them;

  • nothing about failure — a failed coercion, an out-of-bounds index and an empty-array construction all simply have no derivation, and remark 139.5 is the reason.

Adding rules that propagate dynamic errors, and then proving termination by a logical relation in the style of Tait’s normalization argument, is a separate development. Empty arrays are excluded, and admitting them requires shape information to be available dynamically wherever an array can be empty.

Exercise 139.4

★★☆ Exhibit a well-typed closed expression of F with no evaluation derivation, and conclude that theorem 139.15 cannot be strengthened to a progress statement without changing definition 139.4.

System card: a production array compiler

Only now, with the proved core in hand, is it useful to look at an implementation. The following observations are about a particular compiler and are not theorems about F.

Second-order array combinators.

The source language offers map, reduce, scan and filter as primitives with fixed sequential meanings. Their sequential semantics is what the compiler is entitled to preserve; their parallel implementation is a choice made later.

Uniqueness and aliasing.

A parameter marked unique may be consumed, so its buffer can be updated in place. The analysis tracks which values may alias which, and rejects a program that uses a consumed value. This is a static discipline for in-place update in a pure language, and it is absent from F: definition 139.1 has no update and no uniqueness annotation.

Fusion and flattening.

Adjacent combinators are fused into one, and nested parallelism is flattened into flat parallelism. Each pass has an invariant that the compiler’s own type checker enforces. That a pass preserves that invariant is evidence of the kind discussed in chapter 133, and is not a semantic-preservation theorem.

Generated code.

The back end emits code for a parallel target. The boundary between the functional meaning of the source and the behaviour of the generated program is where floating-point associativity, scheduling and hardware behaviour enter, and none of them is modelled by F.

Bounded property analysis.

A restricted property language can express equivalence, range, injectivity, bijectivity, monotonicity, filtering and partitioning of array-producing expressions, and a bounded automatic analysis decides properties in that grammar. It is an analysis with a stated grammar, not a verifier.

Remark 139.17 — What is not claimed

Nothing above asserts end-to-end compiler correctness, floating-point equivalence between source and target, race freedom in the presence of foreign code, asymptotic optimality, or correctness of the hardware. The cited language definitions and implementation studies establish none of those five statements.

Comparison card: eager memoized functions over index sets

Change one design decision at a time from the proved spine.

  1. Replace “array indexed by a size” with “eagerly memoized function over a typed finite index set”. A table has type ia where i is a type whose inhabitants are the legal indices. Where F writes [n]τ, this design writes Fin nτ, and the index type may be a sum or a product rather than a prefix of the integers.

  2. Replace bulk combinators with pointful nested indexing. A map becomes a comprehension over the index set, and the programmer writes the element expression rather than a function to be mapped.

  3. Add fine-grained effect rows for the operations that behave like mutation. A table built by writing its entries needs an effect; the row records which region is written.

  4. Isolate associative accumulation as its own effect. An accumulator whose combining operation is associative may be split and recombined.

  5. Calculate one reduction or histogram with that accumulator, showing the reordering argument for that example.

Remark 139.18 — The boundary of the comparison

This card uses the typed-index, effect and accumulation material of its source and nothing else. It claims no general race-freedom theorem: item 5 is a calculation about one example, licensed by associativity of that particular operation. It claims no progress or preservation theorem for the compared language, and no correctness of any of its compiler passes. The automatic differentiation of that line of work, and the separate linearization, unzipping and transposition results, are reserved for the differentiation route and are not used here. Upstream describes the implementation as an early-stage research project.

Exercise 139.5

★★☆ Express the type of zip from example 139.8 in the index-set design of section 139.6, and say which of the two designs makes the equality of the two lengths a typing question and which makes it a question about the index type.

Limits and seminar

The frozen source for the formal core is Bailly, Henriksen and Elsman (2023): definition 139.1, definition 139.2, definition 139.3, definition 139.4, definition 139.6, definition 139.11 are its Figures 1–7, and proposition 139.7, proposition 139.12, proposition 139.13, proposition 139.14 and theorem 139.15 are its Propositions 3.4, 3.5, 3.6–3.8, 3.9 and 3.10, at their exact statements. The 2021 precursor is used only where signatures coincide. Section 139.5 reports the implementation observations of the compiler papers, the thesis and the bounded property analysis; section 139.6 reports the typed-index, effect and accumulation design of its own source. No theorem is transferred from an implementation to F, or from F to an implementation.

Five boundaries. Theorem 139.15 is conditioned on a successful evaluation, by remark 139.16, and is neither progress nor termination. Arrays are nonempty, and proposition 139.13 uses that. Size equality is syntactic, so example 139.9 needs a coercion with a run-time check where a solver would not. Uniqueness, in-place update, fusion, flattening and code generation are outside F entirely, and remark 139.17 lists the five statements that are not proved. Finally, the printed formal syntax is F; any presentation-only notation used in an example must elaborate to it by an explicit translation with its own preservation lemma, and similar notation is not enough.

[4]

Suggested first pass.

None of these problems is a prerequisite for a later chapter. Begin with exercise 139.6, then complete exercise 139.9.

Exercise 139.6

★★☆ Derive the type of zip n a b in example 139.8 step by step, naming the rule at each node, and then show that replacing b by an array of declared length m leaves no derivation. Identify the exact premise that fails.

Exercise 139.7

★★★ t-relax may be applied at any point, so a program has many typing derivations. Show that any two derivations of the same program give types that differ only in size information, and give two derivations of one program whose types are not syntactically equal.

Exercise 139.8

★★★ Admit empty arrays into definition 139.1. Show that clause 1 of proposition 139.13 becomes false, by exhibiting a value, a type and a witnessed variable for which no size can be extracted. Then propose the smallest change to definition 139.4 that restores it, and say what it costs at run time.

Exercise 139.9 — Practical: size-dependent checker and evaluator

★★★ Practical project.arr-size-checker Complete project arr-size-checker. Implement, for the frozen core of definition 139.1, (i) the typing rules of definition 139.3 including t-map’s side condition, t-let’s witness condition and t-coerce, (ii) the big-step evaluator of definition 139.4 with dynamic size matching and dynamic size checking, and (iii) the witness function of definition 139.2. The invariant the implementation must maintain is theorem 139.15 on every accepted input: whenever the checker accepts e at μ and the evaluator produces v, the value must satisfy the size constraints μ names. Exclude empty arrays and failure propagation: either would be a separately proved extension beyond theorem 139.15. The frozen core has no filter and no recursion, so produce the escaping size of example 139.10 by relaxing a known size into an existential and unpacking it; record that this is weaker than a computation whose result length is genuinely unknown. The named cases print

dot-product: 32
mismatched-zip: rejected
unpacked-length: 3
unwitnessed-size: rejected
map-ragged: rejected

The checker is independent evidence for the frozen core; it does not prove theorem 139.15, implements none of section 139.5 or section 139.6, and — by remark 139.16 — says nothing about programs whose evaluation fails.

Search the book

Type to search the local edition.